diff --git a/.github/workflows/publish-dev-custom.yml b/.github/workflows/publish-dev-custom.yml index c5760a1d..5de68e4e 100644 --- a/.github/workflows/publish-dev-custom.yml +++ b/.github/workflows/publish-dev-custom.yml @@ -86,7 +86,7 @@ jobs: --push \ --platform "$platform" \ --build-arg BINARY_NAME="$binary" \ - --tag "f0rc3/gokapi:tmp-${binary}" \ + --tag "f0rc3/gokapi:tmp-${{ github.event.inputs.tagname }}-${binary}" \ --file Dockerfile.multiarch \ . done @@ -95,7 +95,7 @@ jobs: --tag f0rc3/gokapi:${{ github.event.inputs.tagname }} \ $(for platform in "${!PLATFORMS[@]}"; do binary="${PLATFORMS[$platform]}" - echo "f0rc3/gokapi:tmp-${binary}" + echo "f0rc3/gokapi:tmp-${{ github.event.inputs.tagname }}-${binary}" done) # Delete intermediate tags @@ -107,5 +107,5 @@ jobs: for binary in "${PLATFORMS[@]}"; do curl -s -X DELETE \ -H "Authorization: JWT ${TOKEN}" \ - "https://hub.docker.com/v2/repositories/f0rc3/gokapi/tags/tmp-${binary}/" + "https://hub.docker.com/v2/repositories/f0rc3/gokapi/tags/tmp-${{ github.event.inputs.tagname }}-${binary}/" done diff --git a/.github/workflows/publish-dev.yml b/.github/workflows/publish-dev.yml index a0a7113f..2b72923b 100644 --- a/.github/workflows/publish-dev.yml +++ b/.github/workflows/publish-dev.yml @@ -1,4 +1,4 @@ -name: Build Core Docker - Custom Dev +name: Build Core Docker - Dev Release on: workflow_dispatch: diff --git a/build/go-generate/updateApiRouting.go b/build/go-generate/updateApiRouting.go index a7ddeb33..1f8520c5 100644 --- a/build/go-generate/updateApiRouting.go +++ b/build/go-generate/updateApiRouting.go @@ -10,6 +10,7 @@ import ( "go/token" "log" "os" + "strconv" "strings" ) @@ -37,9 +38,11 @@ func findDeclaredTypes(filePath string) ([]*ast.TypeSpec, error) { // Traverse the AST to find the struct definitions (type declarations) for _, decl := range node.Decls { - if genDecl, ok := decl.(*ast.GenDecl); ok && genDecl.Tok == token.TYPE { + genDecl, ok := decl.(*ast.GenDecl) + if ok && genDecl.Tok == token.TYPE { for _, spec := range genDecl.Specs { - if typeSpec, ok := spec.(*ast.TypeSpec); ok { + typeSpec, ok := spec.(*ast.TypeSpec) + if ok { if strings.HasPrefix(typeSpec.Name.String(), "param") { declaredTypes = append(declaredTypes, typeSpec) } @@ -51,21 +54,64 @@ func findDeclaredTypes(filePath string) ([]*ast.TypeSpec, error) { return declaredTypes, nil } -func hasTags(fields []*ast.Field) bool { +// hasParsableTags returns true if any field has a "header:" or "json:" struct tag. +func hasParsableTags(fields []*ast.Field) bool { + return hasHeaderTags(fields) || hasJsonTags(fields) || hasHttpRequestTags(fields) || hasPostFormTags(fields) +} + +// hasHeaderTags returns true if any field has a "header:" struct tag. +func hasHeaderTags(fields []*ast.Field) bool { for _, field := range fields { if field.Tag != nil { - // Extract the header tag by accessing the field.Tag.Value - tag := field.Tag.Value - if tag != "" { - // Remove backticks - tag = tag[1 : len(tag)-1] + tag := field.Tag.Value[1 : len(field.Tag.Value)-1] + for _, part := range strings.Split(tag, " ") { + if strings.HasPrefix(part, "header:") { + return true + } + } + } + } + return false +} - // Check if the tag has the "header" key and extract its value - tagParts := strings.Split(tag, " ") - for _, part := range tagParts { - if strings.HasPrefix(part, "header:") { - return true - } +// hasJsonTags returns true if any field has a "json:" struct tag. +func hasJsonTags(fields []*ast.Field) bool { + for _, field := range fields { + if field.Tag != nil { + tag := field.Tag.Value[1 : len(field.Tag.Value)-1] + for _, part := range strings.Split(tag, " ") { + if strings.HasPrefix(part, "json:") { + return true + } + } + } + } + return false +} + +// hasHttpRequestTags returns true if any field has a "isHttpRequest:" struct tag. +func hasHttpRequestTags(fields []*ast.Field) bool { + for _, field := range fields { + if field.Tag != nil { + tag := field.Tag.Value[1 : len(field.Tag.Value)-1] + for _, part := range strings.Split(tag, " ") { + if strings.HasPrefix(part, "isHttpRequest:") { + return true + } + } + } + } + return false +} + +// hasPostFormTags returns true if any field has a "postForm:" struct tag. +func hasPostFormTags(fields []*ast.Field) bool { + for _, field := range fields { + if field.Tag != nil { + tag := field.Tag.Value[1 : len(field.Tag.Value)-1] + for _, part := range strings.Split(tag, " ") { + if strings.HasPrefix(part, "postForm:") { + return true } } } @@ -99,32 +145,240 @@ func headerExists(headerName string, required, isString, base64Support bool) str base64SupportEntry = ", has base64support" } return fmt.Sprintf("\n"+` - // RequestParser header value %s, required: %v%s - exists, err = checkHeaderExists(r, %s, %v, %v) - if err != nil { - return err - } - p.foundHeaders[%s] = exists`, headerName, required, base64SupportEntry, headerName, required, isString, headerName) + // RequestParser header value %s, required: %v%s + exists, err = checkHeaderExists(r, %s, %v, %v) + if err != nil { + return err + } + p.foundHeaders[%s] = exists`, headerName, required, base64SupportEntry, headerName, required, isString, headerName) } func generateParseRequestMethod(typeName string, fields []*ast.Field) string { // Start generating the ParseRequest method - if !hasTags(fields) { + if !hasParsableTags(fields) { return fmt.Sprintf(` - // ParseRequest parses the header file. As %s has no fields with the - // tag header, this method does nothing, except calling ProcessParameter() + // ParseRequest parses the header file. As %s has no fields with the tags header, + // json or isHttpRequest, this method does nothing except calling ProcessParameter() func (p *%s) ParseRequest(r *http.Request) error { return p.ProcessParameter(r) } %s`, typeName, typeName, writeNewInstanceCode(typeName)) } - method := fmt.Sprintf(`// ParseRequest reads r and saves the passed header values in the %s struct + needsHeader := hasHeaderTags(fields) + needsJson := hasJsonTags(fields) + needsHttpRequest := hasHttpRequestTags(fields) + needsPostForm := hasPostFormTags(fields) + + // Build preamble: header parsing needs foundHeaders + exists; JSON-only still + // needs err for the Decode call. + preamble := "" + if needsHeader { + preamble = `var err error + var exists bool + p.foundHeaders = make(map[string]bool)` + } else { + if needsJson || needsPostForm { + preamble = "var err error" + } + } + + var readValues []string + if needsHttpRequest { + readValues = append(readValues, "HTTP request") + } + if needsHeader { + readValues = append(readValues, "header") + } + if needsJson { + readValues = append(readValues, "JSON") + } + if needsPostForm { + readValues = append(readValues, "POST form") + } + + method := fmt.Sprintf(`// ParseRequest reads r and saves the passed %s values in the %s struct // In the end, ProcessParameter() is called func (p *%s) ParseRequest(r *http.Request) error { - var err error - var exists bool - p.foundHeaders = make(map[string]bool)`, typeName, typeName) + %s`, strings.Join(readValues, " and "), typeName, typeName, preamble) + + // Emit the JSON decode block before individual field assignments. + // A single anonymous struct is decoded once; fields are then assigned individually + // so that required-field checks and the struct's own field names are preserved. + if needsJson { + type jsonField struct { + fieldName string + jsonKey string + fieldType string + required bool + } + var jsonFields []jsonField + for _, field := range fields { + if field.Tag == nil { + continue + } + tag := field.Tag.Value[1 : len(field.Tag.Value)-1] + for _, part := range strings.Split(tag, " ") { + if strings.HasPrefix(part, "json:") { + jsonKey := strings.TrimPrefix(part, "json:") + jsonKey = strings.Trim(jsonKey, "\"") + jsonKey = strings.Split(jsonKey, ",")[0] // strip omitempty etc. + fieldType := field.Type.(*ast.Ident).Name + required := hasRequiredTag(strings.Split(tag, " ")) + jsonFields = append(jsonFields, jsonField{ + fieldName: field.Names[0].Name, + jsonKey: jsonKey, + fieldType: fieldType, + required: required, + }) + } + } + } + + // Build an anonymous intermediate struct matching the JSON shape + intermediateFields := "" + for _, jf := range jsonFields { + intermediateFields += fmt.Sprintf("\n\t\t\t%s %s `json:\"%s\"`", jf.fieldName, jf.fieldType, jf.jsonKey) + } + method += fmt.Sprintf(` + var jsonBody struct {%s + } + err = json.NewDecoder(r.Body).Decode(&jsonBody) + if err != nil { + return err + }`, intermediateFields) + + // Emit required checks followed by assignment for each json field + for _, jf := range jsonFields { + if jf.required { + switch jf.fieldType { + case "string": + method += fmt.Sprintf(` + if jsonBody.%s == "" { + return fmt.Errorf("json field \"%s\" is required") + }`, jf.fieldName, jf.jsonKey) + case "int", "int64": + method += fmt.Sprintf(` + if jsonBody.%s == 0 { + return fmt.Errorf("json field \"%s\" is required") + }`, jf.fieldName, jf.jsonKey) + } + } + method += fmt.Sprintf(` + p.%s = jsonBody.%s`, jf.fieldName, jf.fieldName) + } + } + + // Emit the POST form parsing block. + // Each field is limited to defaultPostFormFieldMaxBytes unless the field + // carries a maxPostBytes tag specifying a higher limit. + // The overall body is also capped at the largest per-field limit before + // ParseMultipartForm is called, so oversized requests are rejected early. + if needsPostForm { + const defaultPostFormFieldMaxBytes = 1024 // 1 KB + + type postFormField struct { + fieldName string + formKey string + fieldType string + required bool + maxPostBytes int64 // per-field size limit in bytes + } + var postFormFields []postFormField + for _, field := range fields { + if field.Tag == nil { + continue + } + tag := field.Tag.Value[1 : len(field.Tag.Value)-1] + tagParts := strings.Split(tag, " ") + for _, part := range tagParts { + if strings.HasPrefix(part, "postForm:") { + formKey := strings.Trim(strings.TrimPrefix(part, "postForm:"), "\"") + fieldType := field.Type.(*ast.Ident).Name + required := hasRequiredTag(tagParts) + fieldMax := int64(defaultPostFormFieldMaxBytes) + for _, p2 := range tagParts { + if strings.HasPrefix(p2, "maxPostMb:") { + raw := strings.Trim(strings.TrimPrefix(p2, "maxPostMb:"), "\"") + n, err := strconv.Atoi(raw) + if err == nil && n > 0 { + fieldMax = int64(n) * 1024 * 1024 + } + } + } + postFormFields = append(postFormFields, postFormField{ + fieldName: field.Names[0].Name, + formKey: formKey, + fieldType: fieldType, + required: required, + maxPostBytes: fieldMax, + }) + } + } + } + + // Derive the total body limit from the largest individual field limit. + // This is a conservative upper bound — the per-field checks below are + // the authoritative enforcement. + var totalLimit int64 + for _, pf := range postFormFields { + if pf.maxPostBytes > totalLimit { + totalLimit = pf.maxPostBytes + } + } + method += fmt.Sprintf(` + r.Body = http.MaxBytesReader(nil, r.Body, %d) + err = r.ParseMultipartForm(int64(configuration.Get().MaxMemory) * 1024 * 1024) + if err != nil { + return err + }`, totalLimit) + + for _, pf := range postFormFields { + switch pf.fieldType { + case "string": + if pf.required { + method += fmt.Sprintf(` + if r.FormValue(%q) == "" { + return fmt.Errorf("post form field \"%s\" is required") + }`, pf.formKey, pf.formKey) + } + method += fmt.Sprintf(` + if len(r.FormValue(%q)) > %d { + return fmt.Errorf("post form field \"%s\" exceeds maximum length of %d bytes") + } + p.%s = r.FormValue(%q)`, pf.formKey, pf.maxPostBytes, pf.formKey, pf.maxPostBytes, pf.fieldName, pf.formKey) + case "int", "int64": + parseExpr := fmt.Sprintf(`strconv.Atoi(r.FormValue(%q))`, pf.formKey) + assignExpr := fmt.Sprintf("p.%s", pf.fieldName) + if pf.fieldType == "int64" { + parseExpr = fmt.Sprintf(`strconv.ParseInt(r.FormValue(%q), 10, 64)`, pf.formKey) + } + if pf.required { + method += fmt.Sprintf(` + if r.FormValue(%q) == "" { + return fmt.Errorf("post form field \"%s\" is required") + }`, pf.formKey, pf.formKey) + } + method += fmt.Sprintf(` + if r.FormValue(%q) != "" { + %s, err = %s + if err != nil { + return fmt.Errorf("invalid value in post form field \"%s\"") + } + }`, pf.formKey, assignExpr, parseExpr, pf.formKey) + case "bool": + method += fmt.Sprintf(` + if r.FormValue(%q) != "" { + p.%s, err = strconv.ParseBool(r.FormValue(%q)) + if err != nil { + return fmt.Errorf("invalid value in post form field \"%s\"") + } + }`, pf.formKey, pf.fieldName, pf.formKey, pf.formKey) + default: + panic("unsupported postForm field type: " + pf.fieldType) + } + } + } // Iterate over the fields and generate parsing logic for those with a header tag for _, field := range fields { @@ -140,6 +394,9 @@ func generateParseRequestMethod(typeName string, fields []*ast.Field) string { required := hasRequiredTag(tagParts) base64Support := hasBase64Tag(tagParts) for _, part := range tagParts { + if strings.HasPrefix(part, "isHttpRequest:") { + method += fmt.Sprintf("\np.%s = r", field.Names[0].Name) + } if strings.HasPrefix(part, "header:") { // Extract the header name after 'header:' headerName := strings.TrimPrefix(part, "header:") @@ -264,10 +521,39 @@ func main() { var output strings.Builder - output.WriteString(`// Code generated by updateApiRouting.go - DO NOT EDIT. + // Conditionally import "encoding/json" only when at least one struct uses + // json tags, to avoid an unused-import compile error in the generated file. + needsJsonImport := false + needsStrconvImport := false + needsConfigImport := false + for _, typeSpec := range types { + if structType, ok := typeSpec.Type.(*ast.StructType); ok { + if hasJsonTags(structType.Fields.List) { + needsJsonImport = true + } + if hasPostFormTags(structType.Fields.List) { + needsStrconvImport = true + needsConfigImport = true + } + } + } + + jsonImport := "" + if needsJsonImport { + jsonImport = "\n\t\"encoding/json\"" + } + strconvImport := "" + if needsStrconvImport { + strconvImport = "\n\t\"strconv\"" + } + if needsConfigImport { + strconvImport += "\n\t\"github.com/forceu/gokapi/internal/configuration\"" + } + + output.WriteString(fmt.Sprintf(`// Code generated by updateApiRouting.go - DO NOT EDIT. package api - import ( + import (%s%s "encoding/base64" "fmt" "net/http" @@ -277,7 +563,7 @@ func main() { // Do not modify: This is an automatically generated file created by updateApiRouting.go // It contains the code that is used to parse the headers submitted in an API request - `) + `, jsonImport, strconvImport)) // Process each struct type for _, typeSpec := range types { diff --git a/docs/setup.rst b/docs/setup.rst index 3da070a8..7f6634a4 100644 --- a/docs/setup.rst +++ b/docs/setup.rst @@ -254,6 +254,7 @@ This disables all of Gokapi's internal authentication except for API calls. The - ``/e2eSetup`` - ``/filerequests`` - ``/logs`` +- ``/paste`` - ``/uploadChunk`` - ``/uploadStatus`` - ``/users`` diff --git a/internal/configuration/database/provider/sqlite/Sqlite.go b/internal/configuration/database/provider/sqlite/Sqlite.go index 5064fcdc..3197eb82 100644 --- a/internal/configuration/database/provider/sqlite/Sqlite.go +++ b/internal/configuration/database/provider/sqlite/Sqlite.go @@ -22,7 +22,7 @@ type DatabaseProvider struct { } // DatabaseSchemeVersion contains the version number to be expected from the current database. If lower, an upgrade will be performed -const DatabaseSchemeVersion = 15 +const DatabaseSchemeVersion = 16 // New returns an instance func New(dbConfig models.DbConnection) (DatabaseProvider, error) { @@ -110,10 +110,15 @@ func (p DatabaseProvider) Upgrade(currentDbVersion int) { } } } - // < v2.2.5 + // < v2.3.0-dev if currentDbVersion < 15 { p.DeleteAllSessions() } + // < v2.3.0 + if currentDbVersion < 16 { + err := p.rawSqlite(`ALTER TABLE FileMetaData ADD COLUMN "IsPaste" INTEGER NOT NULL DEFAULT 0;`) + helper.Check(err) + } } // GetDbVersion gets the version number of the database @@ -225,6 +230,7 @@ func (p DatabaseProvider) createNewDatabase() error { "UploadDate" INTEGER NOT NULL, "PendingDeletion" INTEGER NOT NULL, "UploadRequestId" TEXT NOT NULL, + "IsPaste" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY("Id") ); CREATE TABLE "Hotlinks" ( diff --git a/internal/configuration/database/provider/sqlite/metadata.go b/internal/configuration/database/provider/sqlite/metadata.go index ef8f8791..7916f14a 100644 --- a/internal/configuration/database/provider/sqlite/metadata.go +++ b/internal/configuration/database/provider/sqlite/metadata.go @@ -30,6 +30,7 @@ type schemaMetaData struct { UploadDate int64 PendingDeletion int64 UploadRequestId string + IsPaste int } func (rowData schemaMetaData) ToFileModel() (models.File, error) { @@ -53,6 +54,7 @@ func (rowData schemaMetaData) ToFileModel() (models.File, error) { UploadDate: rowData.UploadDate, PendingDeletion: rowData.PendingDeletion, UploadRequestId: rowData.UploadRequestId, + IsPaste: rowData.IsPaste == 1, } buf := bytes.NewBuffer(rowData.Encryption) @@ -72,7 +74,8 @@ func (p DatabaseProvider) GetAllMetadata() map[string]models.File { err = rows.Scan(&rowData.Id, &rowData.Name, &rowData.Size, &rowData.SHA1, &rowData.ExpireAt, &rowData.SizeBytes, &rowData.DownloadsRemaining, &rowData.DownloadCount, &rowData.PasswordHash, &rowData.HotlinkId, &rowData.ContentType, &rowData.AwsBucket, &rowData.Encryption, &rowData.UnlimitedDownloads, &rowData.UnlimitedTime, &rowData.UserId, - &rowData.UploadDate, &rowData.PendingDeletion, &rowData.UploadRequestId) + &rowData.UploadDate, &rowData.PendingDeletion, &rowData.UploadRequestId, &rowData.IsPaste) + helper.Check(err) helper.Check(err) var metaData models.File metaData, err = rowData.ToFileModel() @@ -92,7 +95,7 @@ func (p DatabaseProvider) GetMetaDataById(id string) (models.File, bool) { &rowData.DownloadsRemaining, &rowData.DownloadCount, &rowData.PasswordHash, &rowData.HotlinkId, &rowData.ContentType, &rowData.AwsBucket, &rowData.Encryption, &rowData.UnlimitedDownloads, &rowData.UnlimitedTime, &rowData.UserId, &rowData.UploadDate, - &rowData.PendingDeletion, &rowData.UploadRequestId) + &rowData.PendingDeletion, &rowData.UploadRequestId, &rowData.IsPaste) if err != nil { if errors.Is(err, sql.ErrNoRows) { return result, false @@ -132,6 +135,9 @@ func (p DatabaseProvider) SaveMetaData(file models.File) { if file.UnlimitedTime { newData.UnlimitedTime = 1 } + if file.IsPaste { + newData.IsPaste = 1 + } var buf bytes.Buffer enc := gob.NewEncoder(&buf) @@ -141,12 +147,12 @@ func (p DatabaseProvider) SaveMetaData(file models.File) { _, err = p.sqliteDb.Exec(`INSERT OR REPLACE INTO FileMetaData (Id, Name, Size, SHA1, ExpireAt, SizeBytes, DownloadsRemaining, DownloadCount, PasswordHash, HotlinkId, ContentType, AwsBucket, Encryption, - UnlimitedDownloads, UnlimitedTime, UserId, UploadDate, PendingDeletion, UploadRequestId) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,?)`, + UnlimitedDownloads, UnlimitedTime, UserId, UploadDate, PendingDeletion, UploadRequestId, IsPaste) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, newData.Id, newData.Name, newData.Size, newData.SHA1, newData.ExpireAt, newData.SizeBytes, newData.DownloadsRemaining, newData.DownloadCount, newData.PasswordHash, newData.HotlinkId, newData.ContentType, newData.AwsBucket, newData.Encryption, newData.UnlimitedDownloads, newData.UnlimitedTime, newData.UserId, newData.UploadDate, - newData.PendingDeletion, newData.UploadRequestId) + newData.PendingDeletion, newData.UploadRequestId, newData.IsPaste) helper.Check(err) } diff --git a/internal/configuration/setup/ProtectedUrls.go b/internal/configuration/setup/ProtectedUrls.go index 5144976e..6d0d457b 100644 --- a/internal/configuration/setup/ProtectedUrls.go +++ b/internal/configuration/setup/ProtectedUrls.go @@ -6,4 +6,4 @@ package setup // protectedUrls contains a list of URLs that need to be protected if authentication is disabled. // This list will be displayed during the setup -var protectedUrls = []string{"/admin", "/apiKeys", "/auth/token", "/changePassword", "/downloadPresigned", "/e2eSetup", "/filerequests", "/logs", "/uploadChunk", "/uploadStatus", "/users"} +var protectedUrls = []string{"/admin", "/apiKeys", "/auth/token", "/changePassword", "/downloadPresigned", "/e2eSetup", "/filerequests", "/logs", "/paste", "/uploadChunk", "/uploadStatus", "/users"} diff --git a/internal/models/FileList.go b/internal/models/FileList.go index 22326ae9..e68a57b8 100644 --- a/internal/models/FileList.go +++ b/internal/models/FileList.go @@ -31,6 +31,7 @@ type File struct { Encryption EncryptionInfo `json:"Encryption" redis:"-"` // If the file is encrypted, this stores all info for decrypting UnlimitedDownloads bool `json:"UnlimitedDownloads" redis:"UnlimitedDownloads"` // True if the uploader did not limit the downloads UnlimitedTime bool `json:"UnlimitedTime" redis:"UnlimitedTime"` // True if the uploader did not limit the time + IsPaste bool `json:"IsPaste" redis:"IsPaste"` // True if the file is a displayable text paste InternalRedisEncryption []byte `redis:"EncryptionRedis"` // This field is an internal field, used to store the EncryptionInfo in a Redis Hashmap } @@ -59,6 +60,7 @@ type FileApiOutput struct { IsSavedOnLocalStorage bool `json:"IsSavedOnLocalStorage"` // True if the file does not use cloud storage IsPendingDeletion bool `json:"IsPendingDeletion"` // True if the file is about to be deleted IsFileRequest bool `json:"IsFileRequest"` // True if the file belongs to a file request + IsPaste bool `json:"IsPaste"` // True if the file is a pastebin-style text paste UploaderId int `json:"UploaderId"` // The user ID of the uploader } @@ -108,6 +110,9 @@ func (f *File) ToFileApiOutput(serverUrl string, useFilenameInUrl bool) (FileApi } func getDownloadUrl(input FileApiOutput, serverUrl string, useFilename bool) string { + if input.IsPaste { + return serverUrl + "view?id=" + input.Id + } if useFilename { return serverUrl + "d/" + input.Id + "/" + escapeFilename(input.Name) } diff --git a/internal/models/FileList_test.go b/internal/models/FileList_test.go index ae5a6956..e06ec610 100644 --- a/internal/models/FileList_test.go +++ b/internal/models/FileList_test.go @@ -23,6 +23,7 @@ func TestToJsonResult(t *testing.T) { UploadDate: 1748180908, UserId: 2, DownloadCount: 3, + IsPaste: true, Encryption: EncryptionInfo{ IsEncrypted: true, DecryptionKey: []byte{0x01}, @@ -32,8 +33,13 @@ func TestToJsonResult(t *testing.T) { UnlimitedTime: true, PendingDeletion: 100, } - test.IsEqualString(t, file.ToJsonResult("serverurl/", false), `{"Result":"OK","FileInfo":{"Id":"testId","Name":"testName","Size":"10 B","HotlinkId":"hotlinkid","ContentType":"text/html","ExpireAtString":"2025-06-25 11:48:28","UrlDownload":"serverurl/d?id=testId","UrlHotlink":"","FileRequestId":"","UploadDate":1748180908,"ExpireAt":1750852108,"SizeBytes":10,"DownloadsRemaining":1,"DownloadCount":3,"UnlimitedDownloads":true,"UnlimitedTime":true,"RequiresClientSideDecryption":true,"IsEncrypted":true,"IsEndToEndEncrypted":false,"IsPasswordProtected":true,"IsSavedOnLocalStorage":false,"IsPendingDeletion":true,"IsFileRequest":false,"UploaderId":2},"IncludeFilename":false}`) - test.IsEqualString(t, file.ToJsonResult("serverurl/", true), `{"Result":"OK","FileInfo":{"Id":"testId","Name":"testName","Size":"10 B","HotlinkId":"hotlinkid","ContentType":"text/html","ExpireAtString":"2025-06-25 11:48:28","UrlDownload":"serverurl/d/testId/testName","UrlHotlink":"","FileRequestId":"","UploadDate":1748180908,"ExpireAt":1750852108,"SizeBytes":10,"DownloadsRemaining":1,"DownloadCount":3,"UnlimitedDownloads":true,"UnlimitedTime":true,"RequiresClientSideDecryption":true,"IsEncrypted":true,"IsEndToEndEncrypted":false,"IsPasswordProtected":true,"IsSavedOnLocalStorage":false,"IsPendingDeletion":true,"IsFileRequest":false,"UploaderId":2},"IncludeFilename":true}`) + test.IsEqualString(t, file.ToJsonResult("serverurl/", false), `{"Result":"OK","FileInfo":{"Id":"testId","Name":"testName","Size":"10 B","HotlinkId":"hotlinkid","ContentType":"text/html","ExpireAtString":"2025-06-25 11:48:28","UrlDownload":"serverurl/view?id=testId","UrlHotlink":"","FileRequestId":"","UploadDate":1748180908,"ExpireAt":1750852108,"SizeBytes":10,"DownloadsRemaining":1,"DownloadCount":3,"UnlimitedDownloads":true,"UnlimitedTime":true,"RequiresClientSideDecryption":true,"IsEncrypted":true,"IsEndToEndEncrypted":false,"IsPasswordProtected":true,"IsSavedOnLocalStorage":false,"IsPendingDeletion":true,"IsFileRequest":false,"IsPaste":true,"UploaderId":2},"IncludeFilename":false}`) + test.IsEqualString(t, file.ToJsonResult("serverurl/", true), `{"Result":"OK","FileInfo":{"Id":"testId","Name":"testName","Size":"10 B","HotlinkId":"hotlinkid","ContentType":"text/html","ExpireAtString":"2025-06-25 11:48:28","UrlDownload":"serverurl/view?id=testId","UrlHotlink":"","FileRequestId":"","UploadDate":1748180908,"ExpireAt":1750852108,"SizeBytes":10,"DownloadsRemaining":1,"DownloadCount":3,"UnlimitedDownloads":true,"UnlimitedTime":true,"RequiresClientSideDecryption":true,"IsEncrypted":true,"IsEndToEndEncrypted":false,"IsPasswordProtected":true,"IsSavedOnLocalStorage":false,"IsPendingDeletion":true,"IsFileRequest":false,"IsPaste":true,"UploaderId":2},"IncludeFilename":true}`) + + file.IsPaste = false + + test.IsEqualString(t, file.ToJsonResult("serverurl/", false), `{"Result":"OK","FileInfo":{"Id":"testId","Name":"testName","Size":"10 B","HotlinkId":"hotlinkid","ContentType":"text/html","ExpireAtString":"2025-06-25 11:48:28","UrlDownload":"serverurl/d?id=testId","UrlHotlink":"","FileRequestId":"","UploadDate":1748180908,"ExpireAt":1750852108,"SizeBytes":10,"DownloadsRemaining":1,"DownloadCount":3,"UnlimitedDownloads":true,"UnlimitedTime":true,"RequiresClientSideDecryption":true,"IsEncrypted":true,"IsEndToEndEncrypted":false,"IsPasswordProtected":true,"IsSavedOnLocalStorage":false,"IsPendingDeletion":true,"IsFileRequest":false,"IsPaste":false,"UploaderId":2},"IncludeFilename":false}`) + test.IsEqualString(t, file.ToJsonResult("serverurl/", true), `{"Result":"OK","FileInfo":{"Id":"testId","Name":"testName","Size":"10 B","HotlinkId":"hotlinkid","ContentType":"text/html","ExpireAtString":"2025-06-25 11:48:28","UrlDownload":"serverurl/d/testId/testName","UrlHotlink":"","FileRequestId":"","UploadDate":1748180908,"ExpireAt":1750852108,"SizeBytes":10,"DownloadsRemaining":1,"DownloadCount":3,"UnlimitedDownloads":true,"UnlimitedTime":true,"RequiresClientSideDecryption":true,"IsEncrypted":true,"IsEndToEndEncrypted":false,"IsPasswordProtected":true,"IsSavedOnLocalStorage":false,"IsPendingDeletion":true,"IsFileRequest":false,"IsPaste":false,"UploaderId":2},"IncludeFilename":true}`) } func TestIsLocalStorage(t *testing.T) { diff --git a/internal/models/FileUpload.go b/internal/models/FileUpload.go index 63291e3e..8322e23c 100644 --- a/internal/models/FileUpload.go +++ b/internal/models/FileUpload.go @@ -4,13 +4,14 @@ package models type UploadParameters struct { UserId int AllowedDownloads int - Expiry int + ExpiryDays int MaxMemory int ExpiryTimestamp int64 RealSize int64 UnlimitedDownload bool UnlimitedTime bool IsEndToEndEncrypted bool + IsPaste bool Password string ExternalUrl string FileRequestId string diff --git a/internal/storage/FileServing.go b/internal/storage/FileServing.go index 9f3fe438..780af099 100644 --- a/internal/storage/FileServing.go +++ b/internal/storage/FileServing.go @@ -100,8 +100,6 @@ func NewFile(fileContent io.Reader, fileHeader *multipart.FileHeader, userId int if !fileWithHashExists { if tempFile != nil { - err = tempFile.Close() - helper.Check(err) err = os.Rename(tempFile.Name(), dataDir+"/"+file.SHA1) helper.Check(err) hasBeenRenamed = true @@ -122,6 +120,53 @@ func NewFile(fileContent io.Reader, fileHeader *multipart.FileHeader, userId int return file, nil } +// NewPaste creates a new file as a paste in the system. Deduplication is disabled for pastes, +// therefore a random string is used as the hash instead of a content hash. +func NewPaste(content []byte, title string, userId int, uploadRequest models.UploadParameters) (models.File, error) { + syntheticHeader := &multipart.FileHeader{Size: int64(len(content))} + var hasBeenRenamed bool + reader, _, tempFile, encInfo := generateHashAndEncrypt(bytes.NewReader(content), syntheticHeader) + defer deleteTempFile(tempFile, &hasBeenRenamed) + + hash := "paste-" + helper.GenerateRandomString(30) + metaData := createNewMetaData(hash, chunking.FileHeader{ + Filename: title, + Size: int64(len(content)), + ContentType: "text/plain; charset=utf-8", + }, userId, uploadRequest) + metaData.Encryption = encInfo + dataDir := configuration.Get().DataDir + + if !metaData.IsLocalStorage() { + _, err := aws.Upload(reader, metaData) + if err != nil { + return models.File{}, err + } + database.SaveMetaData(metaData) + return metaData, nil + } + + if tempFile != nil { + err := os.Rename(tempFile.Name(), dataDir+"/"+metaData.SHA1) + if err != nil { + return models.File{}, err + } + hasBeenRenamed = true + } else { + destinationFile, err := os.OpenFile(dataDir+"/"+metaData.SHA1, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return models.File{}, err + } + defer destinationFile.Close() + _, err = io.Copy(destinationFile, reader) + if err != nil { + return models.File{}, err + } + } + database.SaveMetaData(metaData) + return metaData, nil +} + // isAllowedFileSize returns true if the file is not greater than the allowed filesize func isAllowedFileSize(size int64) bool { return size <= int64(configuration.Get().MaxFileSizeMB)*1024*1024 @@ -248,7 +293,6 @@ func getChunkFileHash(file *os.File, isEndToEndEncryted bool) (string, error) { } func encryptChunkFile(file *os.File, metadata *models.File) (*os.File, error) { - var removeTempFiles = func() { err := file.Close() if err != nil { @@ -311,6 +355,7 @@ func createNewMetaData(hash string, fileHeader chunking.FileHeader, userId int, UnlimitedDownloads: params.UnlimitedDownload, PasswordHash: configuration.HashPassword(params.Password, false, ""), UserId: userId, + IsPaste: params.IsPaste, UploadRequestId: params.FileRequestId, } if params.IsEndToEndEncrypted { @@ -349,9 +394,12 @@ func getEncInfoFromExistingFile(hash string) (models.EncryptionInfo, bool) { } func deleteTempFile(file *os.File, hasBeenRenamed *bool) { - if file != nil && !*hasBeenRenamed { - err := file.Close() - helper.Check(err) + if file == nil { + return + } + err := file.Close() + helper.Check(err) + if !*hasBeenRenamed { err = os.Remove(file.Name()) helper.Check(err) } @@ -449,6 +497,7 @@ func hashFile(input io.Reader, useSalt bool) (string, error) { if err != nil { return "", err } + //TODO replace with HMAC if useSalt { hash.Write([]byte(configuration.Get().Authentication.SaltFiles)) } @@ -618,9 +667,9 @@ func GetFileByHotlink(id string) (models.File, bool) { return GetFile(fileId) } -// ServeFile subtracts a download allowance and serves the file to the browser -// Returns false if the file expired during the request (most likely race condition due to parallel downloads, requires recheckExpiry) -func ServeFile(file models.File, w http.ResponseWriter, r *http.Request, forceDownload, increaseCounter, forceDecryption, recheckExpiry bool) bool { +// tryAcquireDownload locks the file and subtracts a download allowance. +// Returns (file, true) if the file is valid and the download is allowed, (file, false) if the file is expired or the download is not allowed +func tryAcquireDownload(file models.File, recheckExpiry, increaseCounter bool) (models.File, bool) { apimutex.Lock(apimutex.TypeMetaData, file.Id) if recheckExpiry { if !file.UnlimitedDownloads { @@ -628,7 +677,7 @@ func ServeFile(file models.File, w http.ResponseWriter, r *http.Request, forceDo } if IsExpiredFile(file, time.Now().Unix()) { apimutex.Unlock(apimutex.TypeMetaData, file.Id) - return false + return file, false } } if increaseCounter { @@ -638,9 +687,24 @@ func ServeFile(file models.File, w http.ResponseWriter, r *http.Request, forceDo go sse.PublishDownloadCount(file) } apimutex.Unlock(apimutex.TypeMetaData, file.Id) + return file, true +} +func publishDownloadStatistics(file models.File, r *http.Request) { logging.LogDownload(file, r, configuration.Get().SaveIp) go serverstats.AddTraffic(uint64(file.SizeBytes)) +} + +// ServeFile subtracts a download allowance and serves the file to the browser +// Returns false if the file expired during the request (most likely race condition due to parallel downloads, requires recheckExpiry) +func ServeFile(file models.File, w http.ResponseWriter, r *http.Request, forceDownload, increaseCounter, forceDecryption, recheckExpiry bool) bool { + var isAllowed bool + file, isAllowed = tryAcquireDownload(file, recheckExpiry, increaseCounter) + if !isAllowed { + return false + } + + publishDownloadStatistics(file, r) if !file.IsLocalStorage() { // If non-blocking, we are not setting a download complete status as there is no reliable way to @@ -683,6 +747,31 @@ func ServeFile(file models.File, w http.ResponseWriter, r *http.Request, forceDo return true } +var ErrFileExpired = errors.New("file has expired") + +// ServePaste subtracts a download allowance and returns the Paste content +// Returns false if the file expired during the request (most likely race condition due to parallel downloads, requires recheckExpiry) +func ServePaste(file models.File, r *http.Request, increaseCounter, recheckExpiry bool) (string, error) { + var isAllowed bool + file, isAllowed = tryAcquireDownload(file, recheckExpiry, increaseCounter) + if !isAllowed { + return "", ErrFileExpired + } + + publishDownloadStatistics(file, r) + + fileHandler, _, err := getFileHandler(file, configuration.Get().DataDir) + defer fileHandler.Close() + if err != nil { + return "", err + } + content, err := io.ReadAll(fileHandler) + if err != nil { + return "", err + } + return string(content), nil +} + // Returns the filename if unique or a new filename in the format "Name (x).ext" func makeFilenameUnique(filename string, nameMap *map[string]bool) string { ext := filepath.Ext(filename) @@ -713,7 +802,6 @@ func ServeFilesAsZip(files []models.File, filename string, w http.ResponseWriter w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.zip\"", filename)) w.WriteHeader(http.StatusOK) - saveIp := configuration.Get().SaveIp zipWriter := zip.NewWriter(w) defer zipWriter.Close() filenames := make(map[string]bool) @@ -726,8 +814,7 @@ func ServeFilesAsZip(files []models.File, filename string, w http.ResponseWriter } entryWriter, err := zipWriter.CreateHeader(header) helper.Check(err) - logging.LogDownload(file, r, saveIp) - go serverstats.AddTraffic(uint64(file.SizeBytes)) + publishDownloadStatistics(file, r) if !file.IsLocalStorage() { statusId := downloadstatus.SetDownload(file) err = aws.Stream(entryWriter, file) diff --git a/internal/storage/FileServing_test.go b/internal/storage/FileServing_test.go index d8369d62..60b2cb5e 100644 --- a/internal/storage/FileServing_test.go +++ b/internal/storage/FileServing_test.go @@ -173,7 +173,7 @@ func createRawTestFile(content []byte) (multipart.FileHeader, models.UploadParam } request := models.UploadParameters{ AllowedDownloads: 1, - Expiry: 999, + ExpiryDays: 999, ExpiryTimestamp: 2147483600, MaxMemory: 10, } @@ -264,7 +264,7 @@ func TestNewFile(t *testing.T) { } request = models.UploadParameters{ AllowedDownloads: 1, - Expiry: 999, + ExpiryDays: 999, ExpiryTimestamp: 2147483600, MaxMemory: 10, } @@ -297,7 +297,7 @@ func TestNewFile(t *testing.T) { } request = models.UploadParameters{ AllowedDownloads: 1, - Expiry: 999, + ExpiryDays: 999, ExpiryTimestamp: 2147483600, MaxMemory: 10, } @@ -355,7 +355,7 @@ func TestNewFile(t *testing.T) { } request = models.UploadParameters{ AllowedDownloads: 1, - Expiry: 999, + ExpiryDays: 999, ExpiryTimestamp: 2147483600, MaxMemory: 10, } @@ -478,7 +478,7 @@ func TestDuplicateFile(t *testing.T) { uploadRequest := models.UploadParameters{ AllowedDownloads: 5, - Expiry: 5, + ExpiryDays: 5, ExpiryTimestamp: 200000, Password: "1234", UnlimitedDownload: true, diff --git a/internal/webserver/Webserver.go b/internal/webserver/Webserver.go index 6b707e5b..ad454e17 100644 --- a/internal/webserver/Webserver.go +++ b/internal/webserver/Webserver.go @@ -102,12 +102,14 @@ func Start() { loadExpiryImage() mux.Handle("/", filesystemHandler(webserverDir)) - mux.HandleFunc("/auth/token", requireLogin(handleGenerateAuthToken, false, false)) mux.HandleFunc("/admin", requireLogin(showAdminMenu, true, false)) mux.HandleFunc("/api/", processApi) mux.HandleFunc("/apiKeys", requireLogin(showApiAdmin, true, false)) + mux.HandleFunc("/auth/token", requireLogin(handleGenerateAuthToken, false, false)) mux.HandleFunc("/changePassword", requireLogin(changePassword, true, true)) mux.HandleFunc("/d", showDownload) + mux.HandleFunc("/d/{id}/{filename}", redirectFromFilename) + mux.HandleFunc("/dh/{id}/{filename}", downloadFileWithNameInUrl) mux.HandleFunc("/downloadFile", downloadFile) mux.HandleFunc("/downloadPresigned", requireLogin(downloadPresigned, false, false)) mux.HandleFunc("/e2eSetup", requireLogin(showE2ESetup, true, false)) @@ -118,16 +120,16 @@ func Start() { mux.HandleFunc("/hotlink/", showHotlink) // backward compatibility mux.HandleFunc("/index", showIndex) mux.HandleFunc("/login", showLogin) - mux.HandleFunc("/logs", requireLogin(showLogs, true, false)) mux.HandleFunc("/logout", doLogout) + mux.HandleFunc("/logs", requireLogin(showLogs, true, false)) + mux.HandleFunc("/paste", requireLogin(showPaste, true, false)) mux.HandleFunc("/publicUpload", showPublicUpload) mux.HandleFunc("/uploadChunk", requireLogin(uploadChunk, false, false)) mux.HandleFunc("/uploadStatus", requireLogin(sse.GetStatusSSE, false, false)) mux.HandleFunc("/users", requireLogin(showUserAdmin, true, false)) + mux.HandleFunc("/view", showPasteContent) mux.Handle("/main.wasm", gziphandler.GzipHandler(http.HandlerFunc(serveDownloadWasm))) mux.Handle("/e2e.wasm", gziphandler.GzipHandler(http.HandlerFunc(serveE2EWasm))) - mux.HandleFunc("/d/{id}/{filename}", redirectFromFilename) - mux.HandleFunc("/dh/{id}/{filename}", downloadFileWithNameInUrl) addMuxForCustomContent(mux) @@ -435,6 +437,18 @@ func showUploadRequest(w http.ResponseWriter, r *http.Request) { helper.CheckIgnoreTimeout(err) } +// Handling of /paste +// Lists existing pastes for the current user and provides the paste creation UI +func showPaste(w http.ResponseWriter, r *http.Request) { + user, err := authentication.GetUserFromRequest(r) + if err != nil { + panic(err) + } + view := (&AdminView{}).convertGlobalConfig(ViewPaste, user) + err = templateFolder.ExecuteTemplate(w, "paste", view) + helper.CheckIgnoreTimeout(err) +} + // Handling of /api // If the user is authenticated, this menu lists all uploads and enables uploading new files func showApiAdmin(w http.ResponseWriter, r *http.Request) { @@ -552,6 +566,57 @@ type LoginView struct { CustomContent customStatic } +// requirePasswordView checks if it is required to display the password view for this file. +// If a password needs to be entered, isRequired will be true and showPasswordView() must be called +// redirectRequired is true if a correct password was entered, but a redirect is required to prevent +// a confirmation dialog when refreshing the page +func requirePasswordView(file models.File, w http.ResponseWriter, r *http.Request) (isRequired, isIncorrectAttempt bool) { + if file.PasswordHash == "" { + return false, false + } + if isValidPwCookie(r, file) { + return false, false + } + _ = r.ParseForm() + enteredPassword := r.PostForm.Get("password") + if enteredPassword == "" { + return true, false + } + + ip := logging.GetIpAddress(r) + ratelimiter.WaitOnDownloadPassword(ip) + + isValid, isLegacy := configuration.VerifyPassword(enteredPassword, file.PasswordHash, configuration.Get().Authentication.SaltFiles) + if isValid { + // Migrate legacy passwords to the new format + // Will be removed in the future + if isLegacy { + file.PasswordHash = configuration.HashPassword(enteredPassword, false, "") + database.SaveMetaData(file) + } + writeFilePwCookie(w, file) + return false, false + } + return true, true +} + +func showPasswordView(file models.File, w http.ResponseWriter, wasIncorrectAttempt bool) { + config := configuration.Get() + view := DownloadView{ + Id: file.Id, + IsDownloadView: true, + IsPasswordView: true, + IsPaste: file.IsPaste, + PublicName: config.PublicName, + BaseUrl: config.ServerUrl, + IsFailedLogin: wasIncorrectAttempt, + UsesHttps: configuration.UsesHttps(), + CustomContent: customStaticInfo, + } + err := templateFolder.ExecuteTemplate(w, "download_password", view) + helper.CheckIgnoreTimeout(err) +} + // Handling of /d // Checks if a file exists for the submitted ID // If it exists, a download form is shown, or a password needs to be entered. @@ -559,7 +624,7 @@ func showDownload(w http.ResponseWriter, r *http.Request) { addNoCacheHeader(w) keyId := queryUrl(w, r, "id", errorHandling.TypeFileNotFound) file, ok := storage.GetFile(keyId) - if !ok || file.IsFileRequest() { + if !ok || file.IsFileRequest() || file.IsPaste { redirectOnIncorrectId(w, r, "error") return } @@ -588,40 +653,56 @@ func showDownload(w http.ResponseWriter, r *http.Request) { } } - if file.PasswordHash != "" && !isValidPwCookie(r, file) { - _ = r.ParseForm() - enteredPassword := r.PostForm.Get("password") - if enteredPassword == "" { - view.IsPasswordView = true - err := templateFolder.ExecuteTemplate(w, "download_password", view) - helper.CheckIgnoreTimeout(err) - return - } + isPwRequired, isIncorrectAttempt := requirePasswordView(file, w, r) + if isPwRequired { + showPasswordView(file, w, isIncorrectAttempt) + return + } - ip := logging.GetIpAddress(r) - ratelimiter.WaitOnDownloadPassword(ip) - - isValid, isLegacy := configuration.VerifyPassword(enteredPassword, file.PasswordHash, configuration.Get().Authentication.SaltFiles) - if isValid { - // Migrate legacy passwords to the new format - // Will be removed in the future - if isLegacy { - file.PasswordHash = configuration.HashPassword(enteredPassword, false, "") - database.SaveMetaData(file) - } - writeFilePwCookie(w, file) - // redirect so that there is no post data to be resent if user refreshes page - redirect(w, r, "d?id="+file.Id) + err := templateFolder.ExecuteTemplate(w, "download", view) + helper.CheckIgnoreTimeout(err) +} + +// Handling of /view +// Hotlinks an image or returns a static error image if image has expired +func showPasteContent(w http.ResponseWriter, r *http.Request) { + addNoCacheHeader(w) + keyId := queryUrl(w, r, "id", errorHandling.TypeFileNotFound) + file, ok := storage.GetFile(keyId) + if !ok || !file.IsPaste { + redirectOnIncorrectId(w, r, "error") + return + } + + isPwRequired, isIncorrectAttempt := requirePasswordView(file, w, r) + if isPwRequired { + showPasswordView(file, w, isIncorrectAttempt) + return + } + + content, err := storage.ServePaste(file, r, true, true) + if err != nil { + if !errors.Is(err, storage.ErrFileExpired) { + fmt.Println("Error serving paste content: " + err.Error()) + redirectOnIncorrectId(w, r, "error") return } - view.IsFailedLogin = true - view.IsPasswordView = true - err := templateFolder.ExecuteTemplate(w, "download_password", view) - helper.CheckIgnoreTimeout(err) - return } - err := templateFolder.ExecuteTemplate(w, "download", view) + config := configuration.Get() + err = templateFolder.ExecuteTemplate(w, "paste_view", pasteDisplayView{ + IsAdminView: false, + IsDownloadView: true, + BaseUrl: config.ServerUrl, + PublicName: config.PublicName, + PasteContent: content, + Name: file.Name, + Size: file.Size, + Id: file.Id, + Cipher: file.Encryption.DecryptionKey, + IsPasswordView: false, + CustomContent: customStatic{}, + }) helper.CheckIgnoreTimeout(err) } @@ -632,7 +713,7 @@ func showHotlink(w http.ResponseWriter, r *http.Request) { hotlinkId = strings.Replace(hotlinkId, "/h/", "", 1) addNoCacheHeader(w) file, ok := storage.GetFileByHotlink(hotlinkId) - if !ok || file.IsFileRequest() { + if !ok || file.IsFileRequest() || file.IsPaste { w.Header().Set("Content-Type", "image/svg+xml") _, _ = w.Write(imageExpiredPicture) return @@ -733,6 +814,7 @@ type DownloadView struct { ClientSideDecryption bool EndToEndEncryption bool UsesHttps bool + IsPaste bool CustomContent customStatic } @@ -747,6 +829,7 @@ type e2ESetupView struct { // AdminView contains parameters for all admin-related pages type AdminView struct { Items []models.FileApiOutput + Pastes []models.FileApiOutput ApiKeys []models.ApiKey Users []userInfo FileRequests []models.FileRequest @@ -810,6 +893,8 @@ const ( ViewUsers // ViewFileRequests is the identifier for the file request menu ViewFileRequests + // ViewPaste is the identifier for the paste menu + ViewPaste ) // Converts the globalConfig variable to an AdminView struct to pass the infos to @@ -826,6 +911,9 @@ func (u *AdminView) convertGlobalConfig(view int, user models.User) *AdminView { switch view { case ViewMain: for _, element := range database.GetAllMetadata() { + if element.IsFileRequest() || element.IsPaste { + continue + } if element.UserId != user.Id && !user.HasPermissionListOtherUploads() { continue } @@ -871,6 +959,19 @@ func (u *AdminView) convertGlobalConfig(view int, user models.User) *AdminView { } u.Users = append(u.Users, userWithUploads) } + case ViewPaste: + for _, element := range database.GetAllMetadata() { + if !element.IsPaste { + continue + } + if element.UserId != user.Id && !user.HasPermissionListOtherUploads() { + continue + } + fileInfo, err := element.ToFileApiOutput(config.ServerUrl, config.IncludeFilename) + helper.Check(err) + u.Pastes = append(u.Pastes, fileInfo) + } + u.Pastes = sortMetaDataApi(u.Pastes) case ViewFileRequests: for _, fileRequest := range filerequest.GetAll() { // Double-checking if the owner of the file request exists @@ -1073,7 +1174,7 @@ func serveFile(id string, isRootUrl bool, w http.ResponseWriter, r *http.Request addNoCacheHeader(w) savedFile, ok := storage.GetFile(id) - if !ok || savedFile.IsFileRequest() { + if !ok || savedFile.IsFileRequest() || savedFile.IsPaste { if isRootUrl { redirectOnIncorrectId(w, r, "error") } else { @@ -1193,18 +1294,6 @@ type genericView struct { CustomContent customStatic } -// A view containing parameters for an oauth error -type oauthErrorView struct { - IsAdminView bool - IsDownloadView bool - PublicName string - IsAuthDenied bool - ErrorGenericMessage string - ErrorProvidedName string - ErrorProvidedMessage string - CustomContent customStatic -} - // A view containing parameters for the public upload page type publicUploadView struct { IsAdminView bool @@ -1215,3 +1304,19 @@ type publicUploadView struct { CustomContent customStatic FileRequest *models.FileRequest } + +// A view containing parameters for the public upload page +type pasteDisplayView struct { + IsAdminView bool + IsDownloadView bool + EndToEndEncryption bool + BaseUrl string + PublicName string + PasteContent string + Name string + Size string + Id string + Cipher []byte + IsPasswordView bool + CustomContent customStatic +} diff --git a/internal/webserver/Webserver_test.go b/internal/webserver/Webserver_test.go index f5e2bf8a..c52e04d1 100644 --- a/internal/webserver/Webserver_test.go +++ b/internal/webserver/Webserver_test.go @@ -433,10 +433,12 @@ func TestDownloadCorrectPassword(t *testing.T) { t.Parallel() // Submit download page correct password cookies := test.HttpPageResult(t, test.HttpTestConfig{ - Url: "http://127.0.0.1:53843/d?id=jpLXGJKigM4hjtA6T6sN2", - RedirectUrl: "d?id=jpLXGJKigM4hjtA6T6sN2", - Method: "POST", - PostValues: []test.PostBody{{"password", "123"}}, + Url: "http://127.0.0.1:53843/d?id=jpLXGJKigM4hjtA6T6sN2", + ResultCode: 200, + Method: "POST", + IsHtml: true, + RequiredContent: []string{"smallfile", "./downloadFile?id=jpLXGJKigM4hjtA6T6sN2"}, + PostValues: []test.PostBody{{"password", "123"}}, }) pwCookie := "" for _, cookie := range cookies { diff --git a/internal/webserver/api/Api.go b/internal/webserver/api/Api.go index 9fedee13..8ddf3fe6 100644 --- a/internal/webserver/api/Api.go +++ b/internal/webserver/api/Api.go @@ -685,6 +685,34 @@ func createAndOutputPresignedUrl(ids []string, w http.ResponseWriter, filename s _, _ = w.Write(result) } +func apiPasteAdd(w http.ResponseWriter, r requestParser, user models.User) { + request, ok := r.(*paramPasteAdd) + if !ok { + panic("invalid parameter passed") + } + if request.Title == "" { + request.Title = "Untitled Paste" + } + file, err := storage.NewPaste([]byte(request.PasteContent), request.Title, user.Id, models.UploadParameters{ + UserId: user.Id, + AllowedDownloads: request.AllowedDownloads, + ExpiryDays: request.ExpiryDays, + ExpiryTimestamp: time.Now().Add(time.Duration(request.ExpiryDays) * time.Hour * 24).Unix(), + MaxMemory: configuration.Get().MaxMemory, + UnlimitedDownload: request.UnlimitedDownloads, + UnlimitedTime: request.UnlimitedExpiry, + IsEndToEndEncrypted: request.IsEndToEnd, + IsPaste: true, + Password: request.Password, + ExternalUrl: configuration.Get().ServerUrl, + }) + if err != nil { + sendError(w, http.StatusBadRequest, errorcodes.UnspecifiedError, err.Error()) + return + } + outputFileApiInfo(w, file) +} + func apiUploadFile(w http.ResponseWriter, r requestParser, user models.User) { request, ok := r.(*paramFilesAdd) if !ok { diff --git a/internal/webserver/api/routing.go b/internal/webserver/api/routing.go index 6a7cb59e..521bd572 100644 --- a/internal/webserver/api/routing.go +++ b/internal/webserver/api/routing.go @@ -98,6 +98,12 @@ var routes = []apiRoute{ execution: apiUploadFile, RequestParser: ¶mFilesAdd{}, }, + { + Url: "/files/addPaste", + ApiPerm: models.ApiPermUpload, + execution: apiPasteAdd, + RequestParser: ¶mPasteAdd{}, + }, { Url: "/files/delete", ApiPerm: models.ApiPermDelete, @@ -327,14 +333,13 @@ func (p *paramFilesListSingle) ProcessParameter(r *http.Request) error { type paramFilesDownloadSingle struct { Id string - WebRequest *http.Request - IncreaseCounter bool `header:"increaseCounter"` - PresignUrl bool `header:"presignUrl"` + WebRequest *http.Request `isHttpRequest:"true"` + IncreaseCounter bool `header:"increaseCounter"` + PresignUrl bool `header:"presignUrl"` foundHeaders map[string]bool } func (p *paramFilesDownloadSingle) ProcessParameter(r *http.Request) error { - p.WebRequest = r url := parseRequestUrl(r) p.Id = strings.TrimPrefix(url, "/files/download/") return nil @@ -342,11 +347,11 @@ func (p *paramFilesDownloadSingle) ProcessParameter(r *http.Request) error { type paramFilesDownloadZip struct { Ids []string - WebRequest *http.Request - FileIds string `header:"ids" required:"true"` - Filename string `header:"filename" supportBase64:"true"` - IncreaseCounter bool `header:"increaseCounter"` - PresignUrl bool `header:"presignUrl"` + FileIds string `header:"ids" required:"true"` + Filename string `header:"filename" supportBase64:"true"` + IncreaseCounter bool `header:"increaseCounter"` + PresignUrl bool `header:"presignUrl"` + WebRequest *http.Request `isHttpRequest:"true"` foundHeaders map[string]bool } @@ -359,11 +364,28 @@ func (p *paramFilesDownloadZip) ProcessParameter(r *http.Request) error { } type paramFilesAdd struct { - Request *http.Request + Request *http.Request `isHttpRequest:"true"` +} + +func (p *paramFilesAdd) ProcessParameter(_ *http.Request) error { + return nil } -func (p *paramFilesAdd) ProcessParameter(r *http.Request) error { - p.Request = r +type paramPasteAdd struct { + // PasteContent max size 10MB, must never be more than 2048 + PasteContent string `postForm:"pasteContent" required:"true" maxPostMb:"10"` + Title string `postForm:"title"` + AllowedDownloads int `postForm:"allowedDownloads"` + ExpiryDays int `postForm:"expiryDays"` + Password string `postForm:"password"` + IsEndToEnd bool `postForm:"isEndToEnd" unpublished:"true"` // not published in API documentation + UnlimitedDownloads bool + UnlimitedExpiry bool +} + +func (p *paramPasteAdd) ProcessParameter(r *http.Request) error { + p.UnlimitedExpiry = p.ExpiryDays == 0 + p.UnlimitedDownloads = p.AllowedDownloads == 0 return nil } @@ -624,13 +646,12 @@ func (p *paramE2eStore) ProcessParameter(r *http.Request) error { } type paramLogsDelete struct { - Timestamp int64 `header:"timestamp"` - Request *http.Request + Timestamp int64 `header:"timestamp"` + Request *http.Request `isHttpRequest:"true"` foundHeaders map[string]bool } -func (p *paramLogsDelete) ProcessParameter(r *http.Request) error { - p.Request = r +func (p *paramLogsDelete) ProcessParameter(_ *http.Request) error { return nil } @@ -644,11 +665,10 @@ func (p *paramLogsGet) ProcessParameter(_ *http.Request) error { } type paramChunkAdd struct { - Request *http.Request + Request *http.Request `isHttpRequest:"true"` } -func (p *paramChunkAdd) ProcessParameter(r *http.Request) error { - p.Request = r +func (p *paramChunkAdd) ProcessParameter(_ *http.Request) error { return nil } @@ -657,14 +677,13 @@ func (p *paramChunkAdd) GetRequest() *http.Request { } type paramChunkUploadRequestAdd struct { - Request *http.Request - FileRequestId string `header:"fileRequestId" required:"true"` - ApiKey string `header:"apikey" unpublished:"true"` // not published in API documentation + Request *http.Request `isHttpRequest:"true"` + FileRequestId string `header:"fileRequestId" required:"true"` + ApiKey string `header:"apikey" unpublished:"true"` // not published in API documentation foundHeaders map[string]bool } -func (p *paramChunkUploadRequestAdd) ProcessParameter(r *http.Request) error { - p.Request = r +func (p *paramChunkUploadRequestAdd) ProcessParameter(_ *http.Request) error { return nil } func (p *paramChunkUploadRequestAdd) GetRequest() *http.Request { diff --git a/internal/webserver/api/routingParsing.go b/internal/webserver/api/routingParsing.go index 5fa81dee..d8e3ecf8 100644 --- a/internal/webserver/api/routingParsing.go +++ b/internal/webserver/api/routingParsing.go @@ -4,7 +4,9 @@ package api import ( "encoding/base64" "fmt" + "github.com/forceu/gokapi/internal/configuration" "net/http" + "strconv" "strings" ) @@ -39,8 +41,8 @@ func (p *paramFilesListAll) New() requestParser { return ¶mFilesListAll{} } -// ParseRequest parses the header file. As paramFilesListSingle has no fields with the -// tag header, this method does nothing, except calling ProcessParameter() +// ParseRequest parses the header file. As paramFilesListSingle has no fields with the tags header, +// json or isHttpRequest, this method does nothing except calling ProcessParameter() func (p *paramFilesListSingle) ParseRequest(r *http.Request) error { return p.ProcessParameter(r) } @@ -50,12 +52,13 @@ func (p *paramFilesListSingle) New() requestParser { return ¶mFilesListSingle{} } -// ParseRequest reads r and saves the passed header values in the paramFilesDownloadSingle struct +// ParseRequest reads r and saves the passed HTTP request and header values in the paramFilesDownloadSingle struct // In the end, ProcessParameter() is called func (p *paramFilesDownloadSingle) ParseRequest(r *http.Request) error { var err error var exists bool p.foundHeaders = make(map[string]bool) + p.WebRequest = r // RequestParser header value "increaseCounter", required: false exists, err = checkHeaderExists(r, "increaseCounter", false, false) @@ -91,7 +94,7 @@ func (p *paramFilesDownloadSingle) New() requestParser { return ¶mFilesDownloadSingle{} } -// ParseRequest reads r and saves the passed header values in the paramFilesDownloadZip struct +// ParseRequest reads r and saves the passed HTTP request and header values in the paramFilesDownloadZip struct // In the end, ProcessParameter() is called func (p *paramFilesDownloadZip) ParseRequest(r *http.Request) error { var err error @@ -151,6 +154,7 @@ func (p *paramFilesDownloadZip) ParseRequest(r *http.Request) error { } } + p.WebRequest = r return p.ProcessParameter(r) } @@ -159,9 +163,11 @@ func (p *paramFilesDownloadZip) New() requestParser { return ¶mFilesDownloadZip{} } -// ParseRequest parses the header file. As paramFilesAdd has no fields with the -// tag header, this method does nothing, except calling ProcessParameter() +// ParseRequest reads r and saves the passed HTTP request values in the paramFilesAdd struct +// In the end, ProcessParameter() is called func (p *paramFilesAdd) ParseRequest(r *http.Request) error { + + p.Request = r return p.ProcessParameter(r) } @@ -170,6 +176,56 @@ func (p *paramFilesAdd) New() requestParser { return ¶mFilesAdd{} } +// ParseRequest reads r and saves the passed POST form values in the paramPasteAdd struct +// In the end, ProcessParameter() is called +func (p *paramPasteAdd) ParseRequest(r *http.Request) error { + var err error + r.Body = http.MaxBytesReader(nil, r.Body, 10485760) + err = r.ParseMultipartForm(int64(configuration.Get().MaxMemory) * 1024 * 1024) + if err != nil { + return err + } + if r.FormValue("pasteContent") == "" { + return fmt.Errorf("post form field \"pasteContent\" is required") + } + if len(r.FormValue("pasteContent")) > 10485760 { + return fmt.Errorf("post form field \"pasteContent\" exceeds maximum length of 10485760 bytes") + } + p.PasteContent = r.FormValue("pasteContent") + if len(r.FormValue("title")) > 1024 { + return fmt.Errorf("post form field \"title\" exceeds maximum length of 1024 bytes") + } + p.Title = r.FormValue("title") + if r.FormValue("allowedDownloads") != "" { + p.AllowedDownloads, err = strconv.Atoi(r.FormValue("allowedDownloads")) + if err != nil { + return fmt.Errorf("invalid value in post form field \"allowedDownloads\"") + } + } + if r.FormValue("expiryDays") != "" { + p.ExpiryDays, err = strconv.Atoi(r.FormValue("expiryDays")) + if err != nil { + return fmt.Errorf("invalid value in post form field \"expiryDays\"") + } + } + if len(r.FormValue("password")) > 1024 { + return fmt.Errorf("post form field \"password\" exceeds maximum length of 1024 bytes") + } + p.Password = r.FormValue("password") + if r.FormValue("isEndToEnd") != "" { + p.IsEndToEnd, err = strconv.ParseBool(r.FormValue("isEndToEnd")) + if err != nil { + return fmt.Errorf("invalid value in post form field \"isEndToEnd\"") + } + } + return p.ProcessParameter(r) +} + +// New returns a new instance of paramPasteAdd struct +func (p *paramPasteAdd) New() requestParser { + return ¶mPasteAdd{} +} + // ParseRequest reads r and saves the passed header values in the paramFilesChangeOwner struct // In the end, ProcessParameter() is called func (p *paramFilesChangeOwner) ParseRequest(r *http.Request) error { @@ -813,8 +869,8 @@ func (p *paramUserResetPw) New() requestParser { return ¶mUserResetPw{} } -// ParseRequest parses the header file. As paramE2eStore has no fields with the -// tag header, this method does nothing, except calling ProcessParameter() +// ParseRequest parses the header file. As paramE2eStore has no fields with the tags header, +// json or isHttpRequest, this method does nothing except calling ProcessParameter() func (p *paramE2eStore) ParseRequest(r *http.Request) error { return p.ProcessParameter(r) } @@ -824,7 +880,7 @@ func (p *paramE2eStore) New() requestParser { return ¶mE2eStore{} } -// ParseRequest reads r and saves the passed header values in the paramLogsDelete struct +// ParseRequest reads r and saves the passed HTTP request and header values in the paramLogsDelete struct // In the end, ProcessParameter() is called func (p *paramLogsDelete) ParseRequest(r *http.Request) error { var err error @@ -844,6 +900,7 @@ func (p *paramLogsDelete) ParseRequest(r *http.Request) error { } } + p.Request = r return p.ProcessParameter(r) } @@ -880,9 +937,11 @@ func (p *paramLogsGet) New() requestParser { return ¶mLogsGet{} } -// ParseRequest parses the header file. As paramChunkAdd has no fields with the -// tag header, this method does nothing, except calling ProcessParameter() +// ParseRequest reads r and saves the passed HTTP request values in the paramChunkAdd struct +// In the end, ProcessParameter() is called func (p *paramChunkAdd) ParseRequest(r *http.Request) error { + + p.Request = r return p.ProcessParameter(r) } @@ -891,12 +950,13 @@ func (p *paramChunkAdd) New() requestParser { return ¶mChunkAdd{} } -// ParseRequest reads r and saves the passed header values in the paramChunkUploadRequestAdd struct +// ParseRequest reads r and saves the passed HTTP request and header values in the paramChunkUploadRequestAdd struct // In the end, ProcessParameter() is called func (p *paramChunkUploadRequestAdd) ParseRequest(r *http.Request) error { var err error var exists bool p.foundHeaders = make(map[string]bool) + p.Request = r // RequestParser header value "fileRequestId", required: true exists, err = checkHeaderExists(r, "fileRequestId", true, true) @@ -1367,8 +1427,8 @@ func (p *paramURequestSave) New() requestParser { return ¶mURequestSave{} } -// ParseRequest parses the header file. As paramURequestListSingle has no fields with the -// tag header, this method does nothing, except calling ProcessParameter() +// ParseRequest parses the header file. As paramURequestListSingle has no fields with the tags header, +// json or isHttpRequest, this method does nothing except calling ProcessParameter() func (p *paramURequestListSingle) ParseRequest(r *http.Request) error { return p.ProcessParameter(r) } diff --git a/internal/webserver/fileupload/FileUpload.go b/internal/webserver/fileupload/FileUpload.go index bfd80293..690ff5d9 100644 --- a/internal/webserver/fileupload/FileUpload.go +++ b/internal/webserver/fileupload/FileUpload.go @@ -133,7 +133,7 @@ func CreateUploadConfig(allowedDownloads, expiryDays int, password string, unlim settings := configuration.Get() return models.UploadParameters{ AllowedDownloads: allowedDownloads, - Expiry: expiryDays, + ExpiryDays: expiryDays, ExpiryTimestamp: time.Now().Add(time.Duration(expiryDays) * time.Hour * 24).Unix(), Password: password, ExternalUrl: settings.ServerUrl, @@ -161,7 +161,7 @@ func parseConfig(values formOrHeader) (models.UploadParameters, error) { } expiryDaysInt, err := strconv.Atoi(expiryDays) if err != nil { - expiryDaysInt = 14 + expiryDaysInt = 0 } unlimitedDownload := values.Get("isUnlimitedDownload") == "true" diff --git a/internal/webserver/fileupload/FileUpload_test.go b/internal/webserver/fileupload/FileUpload_test.go index 52b7e14b..45672bb0 100644 --- a/internal/webserver/fileupload/FileUpload_test.go +++ b/internal/webserver/fileupload/FileUpload_test.go @@ -43,7 +43,7 @@ func TestParseConfig(t *testing.T) { test.IsEqualInt(t, config.AllowedDownloads, 9) test.IsEqualString(t, config.Password, "123") - test.IsEqualInt(t, config.Expiry, 5) + test.IsEqualInt(t, config.ExpiryDays, 5) config, err = parseConfig(data) test.IsNil(t, err) @@ -54,8 +54,8 @@ func TestParseConfig(t *testing.T) { config, err = parseConfig(data) test.IsNil(t, err) test.IsEqualInt(t, config.AllowedDownloads, 1) - test.IsEqualInt(t, config.Expiry, 14) - test.IsEqualBool(t, config.UnlimitedTime, false) + test.IsEqualInt(t, config.ExpiryDays, 0) + test.IsEqualBool(t, config.UnlimitedTime, true) test.IsEqualBool(t, config.UnlimitedDownload, false) data.allowedDownloads = "0" @@ -92,7 +92,7 @@ func TestProcess(t *testing.T) { test.IsEqualString(t, result.FileInfo.UrlHotlink, "http://127.0.0.1:53843/downloadFile?id="+result.FileInfo.Id) test.IsEqualString(t, result.FileInfo.Name, "testFile") test.IsEqualString(t, result.FileInfo.Size, "11 B") - test.IsEqualBool(t, result.FileInfo.UnlimitedTime, false) + test.IsEqualBool(t, result.FileInfo.UnlimitedTime, true) test.IsEqualBool(t, result.FileInfo.UnlimitedDownloads, false) test.IsEqualInt(t, result.FileInfo.UploaderId, 9) } diff --git a/internal/webserver/web/static/apidocumentation/openapi.json b/internal/webserver/web/static/apidocumentation/openapi.json index 1086d19c..6a80d4cc 100644 --- a/internal/webserver/web/static/apidocumentation/openapi.json +++ b/internal/webserver/web/static/apidocumentation/openapi.json @@ -1377,6 +1377,51 @@ } } }, + "/files/addPaste": { + "post": { + "tags": [ + "files" + ], + "summary": "Creates a new text paste", + "description": "Stores a plain-text paste in Gokapi with optional download limit, expiry and password protection. Requires API permission UPLOAD", + "operationId": "pasteAdd", + "security": [ + { + "apikey": [ + "UPLOAD" + ] + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/PasteAddBody" + } + } + } + }, + "responses": { + "200": { + "description": "Operation successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UploadResult" + } + } + } + }, + "400": { + "description": "Invalid input or missing required fields" + }, + "401": { + "description": "Invalid API key provided for authentication or API key does not have the required permission" + } + } + } + }, "/auth/create": { "post": { "tags": [ @@ -2600,6 +2645,35 @@ "description": "NewUser is the struct used for the result after creating a new API key", "x-go-package": "Gokapi/internal/models" }, + "PasteAddBody": { + "required": [ + "pasteContent" + ], + "type": "object", + "properties": { + "pasteContent": { + "type": "string", + "maxLength": 10485760, + "description": "The plain-text content of the paste" + }, + "title": { + "type": "string", + "description": "Optional title for the paste" + }, + "allowedDownloads": { + "type": "integer", + "description": "How many times the paste can be viewed. Unlimited if 0 or omitted" + }, + "expiryDays": { + "type": "integer", + "description": "How many days the paste will be stored. Unlimited if 0 or omitted" + }, + "password": { + "type": "string", + "description": "Password to protect the paste. No password will be used if empty" + } + } + }, "body": { "required": [ "file" diff --git a/internal/webserver/web/static/css/cover.css b/internal/webserver/web/static/css/cover.css index 70ec72c5..647ab511 100644 --- a/internal/webserver/web/static/css/cover.css +++ b/internal/webserver/web/static/css/cover.css @@ -694,12 +694,33 @@ tr.no-bottom-border td { border: 1px solid #333; border-radius: 1rem; box-shadow: 0 10px 30px rgba(0,0,0,0.5); - /* Removed the transform/hover effect */ max-width: 450px; width: 100%; background: #1e1e1e; color: #e0e0e0; } + .paste-card { + border: 1px solid #333; + border-radius: 1rem; + box-shadow: 0 10px 30px rgba(0,0,0,0.5); + min-width: 300px; + width: 80%; + background: #1e1e1e; + color: #e0e0e0; + } + + + @media (max-width: 767px) { + .paste-card { + width: 100%; + min-width: unset; + border-radius: 0; + border-left: none; + border-right: none; + box-shadow: none; + } +} + .icon-container { width: 70px; height: 70px; @@ -841,3 +862,7 @@ tr.no-bottom-border td { .modal-samesize-input-filerequest { width: 7rem; } + +.placeholder-white::placeholder { + color: #6c757d; +} diff --git a/internal/webserver/web/static/css/min/gokapi.min.ccb62a8d5b.css b/internal/webserver/web/static/css/min/gokapi.min.ccb62a8d5b.css index 65a0a81a..18930c39 100644 --- a/internal/webserver/web/static/css/min/gokapi.min.ccb62a8d5b.css +++ b/internal/webserver/web/static/css/min/gokapi.min.ccb62a8d5b.css @@ -1 +1 @@ -.btn-secondary,.btn-secondary:hover,.btn-secondary:focus{color:#333;text-shadow:none}body{background:url(../../assets/background.jpg)no-repeat 50% fixed;-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-size:cover;display:-ms-flexbox;display:-webkit-box;display:flex;-ms-flex-pack:center;-webkit-box-pack:center;justify-content:center}body::after{content:"";position:fixed;top:0;left:0;width:100%;height:100%;box-shadow:inset 0 0 5rem rgba(0,0,0,.5);pointer-events:none;z-index:10}td{vertical-align:middle;position:relative}a{color:inherit}a:hover{color:inherit;filter:brightness(80%)}.text-muted{color:#adb5bd!important}.card{margin:0 auto;float:none;margin-bottom:10px;border:2px solid #33393f}.card-body{background-color:#212529;color:#ddd}.card-title{font-weight:900}.admin-input{text-align:center}.form-control:disabled{background:#bababa}.break{flex-basis:100%;height:0}.bd-placeholder-img{font-size:1.125rem;text-anchor:middle;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(min-width:768px){.bd-placeholder-img-lg{font-size:3.5rem}.break{flex-basis:0}}.masthead{margin-bottom:2rem}.masthead-brand{margin-bottom:0}.nav-masthead .nav-link{padding:.25rem 0;font-weight:700;color:rgba(255,255,255,.5);background-color:initial;border-bottom:.25rem solid transparent}.nav-masthead .nav-link:hover,.nav-masthead .nav-link:focus{border-bottom-color:rgba(255,255,255,.25)}.nav-masthead .nav-link+.nav-link{margin-left:1rem}.nav-masthead .active{color:#fff;border-bottom-color:#fff}#qroverlay{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.3)}#qrcode{position:absolute;top:50%;left:50%;margin-top:-105px;margin-left:-105px;width:210px;height:210px;border:5px solid #fff}.toastnotification{pointer-events:none;position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background-color:#333;color:#fff;padding:15px;border-radius:5px;box-shadow:0 2px 5px rgba(0,0,0,.3);opacity:0;transition:opacity .3s ease-in-out;z-index:9999}.toastdeprecation{background-color:#8b0000}.toastnotification.show{opacity:1;pointer-events:auto}.toast-undo{margin-left:20px;color:#4fc3f7;cursor:pointer;text-decoration:underline;font-weight:700;pointer-events:auto}.toast-undo:hover{color:#81d4fa}.toastnotification:not(.show){pointer-events:none!important}.toastnotification:not(.show) .toast-undo{pointer-events:none}.perm-granted{cursor:pointer;color:#0edf00}.perm-notgranted{cursor:pointer;color:#9f9999}.perm-unavailable{color:#525252}.perm-processing{pointer-events:none;color:#e5eb00;animation:perm-pulse 1s infinite}.perm-nochange{cursor:default}.perm-granted:not(.perm-nochange):hover{color:#ff4d4d}.perm-notgranted:not(.perm-nochange):hover{color:#4dff4d}.perm-granted:not(.perm-nochange),.perm-notgranted:not(.perm-nochange){transition:color .15s ease,transform .1s ease}@keyframes perm-pulse{0%{opacity:1}50%{opacity:.5}100%{opacity:1}}.perm-nochange:hover{transform:none}.perm-nowgranted{animation:perm-nowgranted-pulse .5s ease forwards}@keyframes perm-nowgranted-pulse{0%{transform:scale(1.15);color:#4dff4d}50%{transform:scale(1.3);color:#080}100%{transform:scale(1.15);color:#0edf00}}.perm-nownotgranted{animation:perm-nownotgranted-pulse .5s ease forwards}@keyframes perm-nownotgranted-pulse{0%{transform:scale(1.15);color:#ff4d4d}50%{transform:scale(1.3);color:red}100%{transform:scale(1.15);color:##9f9999}}.prevent-select{-webkit-user-select:none;-ms-user-select:none;user-select:none}.gokapi-dialog{background-color:#212529;color:#ddd}@keyframes subtleHighlight{0%{background-color:#444950}100%{background-color:initial}}@keyframes subtleHighlightNewJson{0%{background-color:green}100%{background-color:initial}}.updatedDownloadCount{animation:subtleHighlight .5s ease-out}.newFileRequest{animation:subtleHighlightNewJson .7s ease-out}.newApiKey{animation:subtleHighlightNewJson .7s ease-out}.newUser{animation:subtleHighlightNewJson .7s ease-out}.newItem{animation:subtleHighlightNewJson 1.5s ease-out}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}.rowDeleting{animation:fadeOut .3s ease-out forwards}.highlighted-password{background-color:#444;color:#ddd;padding:2px 6px;border-radius:4px;font-weight:700;font-family:monospace;display:inline-block;margin-left:8px;border:1px solid #555}.filelist-item{background-color:rgba(255,255,255,4%)}.filelist-item:hover{background-color:rgba(255,255,255,8%)}tr.no-bottom-border td{border-bottom:none}.filerequest-item:hover>td{background-color:rgba(255,255,255,8%)}.filerequest-item>td{transition:background-color .15s ease-in-out}.collapse-toggle i{display:inline-block;transition:transform .2s ease}.collapse-toggle[aria-expanded=true] i{transform:rotate(180deg)}.collapse-toggle:hover{opacity:.8}.collapse-toggle{padding:.25rem}.remove-entry-btn:hover{opacity:.8}.info-box{background-color:rgba(255,255,255,5%);border-radius:6px;padding:1rem;margin-bottom:1.5rem;text-align:left}.info-box h6{margin-bottom:.5rem}.info-box ul{margin-bottom:0;padding-left:1.2rem}.callout{padding:20px;margin:10px 20px;border:1px solid #eee;border-left-width:5px;border-radius:3px;h4{margin-top:0;margin-bottom:5px}p:last-child{margin-bottom:0}code{border-radius:3px}&+.bs-callout{margin-top:-5px}}.upload-box{background:#212529;border:2px dashed rgba(255,255,255,.2);border-radius:8px;padding:2rem;transition:all .2s ease;cursor:pointer;display:block;transition:background-color .2s ease}.upload-box.highlight,.upload-box.dz-drag-hover{border-color:#0d6efd;background-color:rgba(13,110,253,5%)}.upload-box:hover{background-color:rgba(255,255,255,5%)}.pu-file-list{margin-top:1.5rem;padding:0 1rem}.pu-file-item{display:grid;gap:.5rem;align-items:center;padding:.75rem 0;border-bottom:1px solid rgba(255,255,255,.1);font-size:.95rem;grid-template-columns:1fr auto;grid-template-areas:"name button" "bar bar" "status size"}.pu-file-item .file-name{grid-area:name;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500;min-width:0;text-align:left}.pu-file-item .upload-status{grid-area:status;font-size:.85rem;opacity:.75;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pu-file-item progress{grid-area:bar;width:100%;height:6px;border-radius:3px;border:none;appearance:none;-webkit-appearance:none;background-color:rgba(255,255,255,.1)}.pu-file-item progress::-webkit-progress-bar{background-color:rgba(255,255,255,.1);border-radius:3px}.pu-file-item progress::-webkit-progress-value{background-color:#0d6efd;border-radius:3px;transition:width .2s ease}.pu-file-item progress::-moz-progress-bar{background-color:#0d6efd;border-radius:3px}.pu-file-item .file-size{grid-area:size;text-align:right;font-size:.85rem;opacity:.75;white-space:nowrap}.pu-file-item button{grid-area:button;padding:.25rem .5rem}@media(min-width:768px){.pu-file-item{grid-template-columns:1fr auto 100px 80px auto;grid-template-areas:"name status bar size button";gap:1rem}.pu-file-item .upload-status{text-align:right;font-size:.95rem;max-width:400px}.pu-file-item .file-size{font-size:.95rem}.pu-file-item progress{height:8px}}.btn-upload-custom{background-color:#0d6efd!important;background-image:none!important;border:none;color:#fff;padding:.8rem 2.5rem;font-weight:600;border-radius:50px;box-shadow:0 4px 12px rgba(0,0,0,.3);transition:all .2s ease-in-out;display:inline-block}.btn-upload-custom:hover:not(:disabled){background-color:#1e7eff!important;transform:translateY(-1px)}.btn-upload-custom:disabled{background-color:#212529!important;color:#6c757d!important;border:1px solid #343a40!important;box-shadow:none;cursor:not-allowed;opacity:1}.stat-card{background:#1a1d20;border:1px solid #2d3238;border-radius:12px;overflow:hidden;transition:transform .2s ease,box-shadow .2s ease}.stat-card:hover{transform:translateY(-3px);box-shadow:0 10px 20px rgba(0,0,0,.3)!important;border-color:#0d6efd}.stat-icon{opacity:.6;font-size:1.2rem}.progress-stat{height:4px;background-color:#2b3035}.log-dark-input{background-color:#2b3035!important;border-color:#444b52!important;color:#e9ecef!important}.log-dark-input:focus{background-color:#32383e!important;border-color:#0d6efd!important;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.input-group-text-dark{background-color:#1a1d20!important;border-color:#444b52!important;color:#adb5bd!important}#logviewer::-webkit-scrollbar{width:8px}#logviewer::-webkit-scrollbar-track{background:#000}#logviewer::-webkit-scrollbar-thumb{background:#333;border-radius:4px}.download-wrapper{min-height:70vh;display:flex;align-items:center;justify-content:center}.file-card{border:1px solid #333;border-radius:1rem;box-shadow:0 10px 30px rgba(0,0,0,.5);max-width:450px;width:100%;background:#1e1e1e;color:#e0e0e0}.icon-container{width:70px;height:70px;background-color:#2d2d2d;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 1.5rem;color:#0d6efd}.filename-text{word-wrap:break-word;color:#fff;font-weight:600}.dark-list-item{background-color:#252525!important;border-color:#333!important;color:#b0b0b0!important}.popover{--bs-popover-bg:#212529;--bs-popover-header-bg:#212529;--bs-popover-header-color:#dee2e6;--bs-popover-body-color:#dee2e6;--bs-popover-body-padding-y:0;--bs-popover-border-color:rgba(255, 255, 255, 0.15);--bs-popover-arrow-border:rgba(255, 255, 255, 0.15)}.popover-header{border-bottom-color:rgba(255,255,255,.15)}.upload-options-section-label{font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:#6c757d;margin-bottom:.5rem}.upload-options-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:.5rem;margin-bottom:.75rem}@media(max-width:767px){.upload-options-grid{grid-template-columns:1fr}}.upload-option-card{background-color:#212529;border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:.5rem .75rem;transition:border-color .2s ease,box-shadow .2s ease}.upload-option-card:has(.upload-option-toggle:checked){border-color:rgba(13,110,253,.5);box-shadow:0 0 0 1px rgba(13,110,253,.2)}.upload-option-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem}.upload-option-icon{font-size:.85rem;color:#6ea8fe;opacity:.9}.upload-option-label{font-size:.72rem;font-weight:600;color:#adb5bd;text-transform:uppercase;letter-spacing:.06em}.upload-option-toggle{cursor:pointer}.upload-option-input-group{margin-top:0}.upload-option-card .form-control{background-color:#2b3035;border-color:rgba(255,255,255,.15);color:#dee2e6;padding:.25rem .5rem;font-size:.875rem}.upload-option-card .form-control:disabled{background-color:#262a2e;color:#555;border-color:rgba(255,255,255,8%)}.upload-option-suffix{background-color:#2b3035;border-color:rgba(255,255,255,.15);color:#adb5bd;font-size:.8rem;padding:.25rem .5rem}.upload-option-card input[type=number]::-webkit-inner-spin-button{filter:invert(1)brightness(.6)}.modal-samesize-input-edit{width:11rem}.modal-samesize-input-filerequest{width:7rem}.filename{font-weight:700;font-size:14px;margin-bottom:5px}.upload-progress-container{display:flex;align-items:center}.upload-progress-bar{position:relative;height:10px;background-color:#eee;flex:1;margin-right:10px;border-radius:4px}.upload-progress-bar-progress{position:absolute;top:0;left:0;height:100%;background-color:#0a0;border-radius:4px;transition:width .2s ease-in-out}.upload-progress-info{font-size:12px}.us-container{margin-top:10px;margin-bottom:20px}.uploaderror{font-weight:700;color:red;margin-bottom:5px}.uploads-container{background-color:#2f343a;border:2px solid rgba(0,0,0,.3);border-radius:5px;margin-left:0;margin-right:0;max-width:none;visibility:hidden} \ No newline at end of file +.btn-secondary,.btn-secondary:hover,.btn-secondary:focus{color:#333;text-shadow:none}body{background:url(../../assets/background.jpg)no-repeat 50% fixed;-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-size:cover;display:-ms-flexbox;display:-webkit-box;display:flex;-ms-flex-pack:center;-webkit-box-pack:center;justify-content:center}body::after{content:"";position:fixed;top:0;left:0;width:100%;height:100%;box-shadow:inset 0 0 5rem rgba(0,0,0,.5);pointer-events:none;z-index:10}td{vertical-align:middle;position:relative}a{color:inherit}a:hover{color:inherit;filter:brightness(80%)}.text-muted{color:#adb5bd!important}.card{margin:0 auto;float:none;margin-bottom:10px;border:2px solid #33393f}.card-body{background-color:#212529;color:#ddd}.card-title{font-weight:900}.admin-input{text-align:center}.form-control:disabled{background:#bababa}.break{flex-basis:100%;height:0}.bd-placeholder-img{font-size:1.125rem;text-anchor:middle;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(min-width:768px){.bd-placeholder-img-lg{font-size:3.5rem}.break{flex-basis:0}}.masthead{margin-bottom:2rem}.masthead-brand{margin-bottom:0}.nav-masthead .nav-link{padding:.25rem 0;font-weight:700;color:rgba(255,255,255,.5);background-color:initial;border-bottom:.25rem solid transparent}.nav-masthead .nav-link:hover,.nav-masthead .nav-link:focus{border-bottom-color:rgba(255,255,255,.25)}.nav-masthead .nav-link+.nav-link{margin-left:1rem}.nav-masthead .active{color:#fff;border-bottom-color:#fff}#qroverlay{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.3)}#qrcode{position:absolute;top:50%;left:50%;margin-top:-105px;margin-left:-105px;width:210px;height:210px;border:5px solid #fff}.toastnotification{pointer-events:none;position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background-color:#333;color:#fff;padding:15px;border-radius:5px;box-shadow:0 2px 5px rgba(0,0,0,.3);opacity:0;transition:opacity .3s ease-in-out;z-index:9999}.toastdeprecation{background-color:#8b0000}.toastnotification.show{opacity:1;pointer-events:auto}.toast-undo{margin-left:20px;color:#4fc3f7;cursor:pointer;text-decoration:underline;font-weight:700;pointer-events:auto}.toast-undo:hover{color:#81d4fa}.toastnotification:not(.show){pointer-events:none!important}.toastnotification:not(.show) .toast-undo{pointer-events:none}.perm-granted{cursor:pointer;color:#0edf00}.perm-notgranted{cursor:pointer;color:#9f9999}.perm-unavailable{color:#525252}.perm-processing{pointer-events:none;color:#e5eb00;animation:perm-pulse 1s infinite}.perm-nochange{cursor:default}.perm-granted:not(.perm-nochange):hover{color:#ff4d4d}.perm-notgranted:not(.perm-nochange):hover{color:#4dff4d}.perm-granted:not(.perm-nochange),.perm-notgranted:not(.perm-nochange){transition:color .15s ease,transform .1s ease}@keyframes perm-pulse{0%{opacity:1}50%{opacity:.5}100%{opacity:1}}.perm-nochange:hover{transform:none}.perm-nowgranted{animation:perm-nowgranted-pulse .5s ease forwards}@keyframes perm-nowgranted-pulse{0%{transform:scale(1.15);color:#4dff4d}50%{transform:scale(1.3);color:#080}100%{transform:scale(1.15);color:#0edf00}}.perm-nownotgranted{animation:perm-nownotgranted-pulse .5s ease forwards}@keyframes perm-nownotgranted-pulse{0%{transform:scale(1.15);color:#ff4d4d}50%{transform:scale(1.3);color:red}100%{transform:scale(1.15);color:##9f9999}}.prevent-select{-webkit-user-select:none;-ms-user-select:none;user-select:none}.gokapi-dialog{background-color:#212529;color:#ddd}@keyframes subtleHighlight{0%{background-color:#444950}100%{background-color:initial}}@keyframes subtleHighlightNewJson{0%{background-color:green}100%{background-color:initial}}.updatedDownloadCount{animation:subtleHighlight .5s ease-out}.newFileRequest{animation:subtleHighlightNewJson .7s ease-out}.newApiKey{animation:subtleHighlightNewJson .7s ease-out}.newUser{animation:subtleHighlightNewJson .7s ease-out}.newItem{animation:subtleHighlightNewJson 1.5s ease-out}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}.rowDeleting{animation:fadeOut .3s ease-out forwards}.highlighted-password{background-color:#444;color:#ddd;padding:2px 6px;border-radius:4px;font-weight:700;font-family:monospace;display:inline-block;margin-left:8px;border:1px solid #555}.filelist-item{background-color:rgba(255,255,255,4%)}.filelist-item:hover{background-color:rgba(255,255,255,8%)}tr.no-bottom-border td{border-bottom:none}.filerequest-item:hover>td{background-color:rgba(255,255,255,8%)}.filerequest-item>td{transition:background-color .15s ease-in-out}.collapse-toggle i{display:inline-block;transition:transform .2s ease}.collapse-toggle[aria-expanded=true] i{transform:rotate(180deg)}.collapse-toggle:hover{opacity:.8}.collapse-toggle{padding:.25rem}.remove-entry-btn:hover{opacity:.8}.info-box{background-color:rgba(255,255,255,5%);border-radius:6px;padding:1rem;margin-bottom:1.5rem;text-align:left}.info-box h6{margin-bottom:.5rem}.info-box ul{margin-bottom:0;padding-left:1.2rem}.callout{padding:20px;margin:10px 20px;border:1px solid #eee;border-left-width:5px;border-radius:3px;h4{margin-top:0;margin-bottom:5px}p:last-child{margin-bottom:0}code{border-radius:3px}&+.bs-callout{margin-top:-5px}}.upload-box{background:#212529;border:2px dashed rgba(255,255,255,.2);border-radius:8px;padding:2rem;transition:all .2s ease;cursor:pointer;display:block;transition:background-color .2s ease}.upload-box.highlight,.upload-box.dz-drag-hover{border-color:#0d6efd;background-color:rgba(13,110,253,5%)}.upload-box:hover{background-color:rgba(255,255,255,5%)}.pu-file-list{margin-top:1.5rem;padding:0 1rem}.pu-file-item{display:grid;gap:.5rem;align-items:center;padding:.75rem 0;border-bottom:1px solid rgba(255,255,255,.1);font-size:.95rem;grid-template-columns:1fr auto;grid-template-areas:"name button" "bar bar" "status size"}.pu-file-item .file-name{grid-area:name;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500;min-width:0;text-align:left}.pu-file-item .upload-status{grid-area:status;font-size:.85rem;opacity:.75;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pu-file-item progress{grid-area:bar;width:100%;height:6px;border-radius:3px;border:none;appearance:none;-webkit-appearance:none;background-color:rgba(255,255,255,.1)}.pu-file-item progress::-webkit-progress-bar{background-color:rgba(255,255,255,.1);border-radius:3px}.pu-file-item progress::-webkit-progress-value{background-color:#0d6efd;border-radius:3px;transition:width .2s ease}.pu-file-item progress::-moz-progress-bar{background-color:#0d6efd;border-radius:3px}.pu-file-item .file-size{grid-area:size;text-align:right;font-size:.85rem;opacity:.75;white-space:nowrap}.pu-file-item button{grid-area:button;padding:.25rem .5rem}@media(min-width:768px){.pu-file-item{grid-template-columns:1fr auto 100px 80px auto;grid-template-areas:"name status bar size button";gap:1rem}.pu-file-item .upload-status{text-align:right;font-size:.95rem;max-width:400px}.pu-file-item .file-size{font-size:.95rem}.pu-file-item progress{height:8px}}.btn-upload-custom{background-color:#0d6efd!important;background-image:none!important;border:none;color:#fff;padding:.8rem 2.5rem;font-weight:600;border-radius:50px;box-shadow:0 4px 12px rgba(0,0,0,.3);transition:all .2s ease-in-out;display:inline-block}.btn-upload-custom:hover:not(:disabled){background-color:#1e7eff!important;transform:translateY(-1px)}.btn-upload-custom:disabled{background-color:#212529!important;color:#6c757d!important;border:1px solid #343a40!important;box-shadow:none;cursor:not-allowed;opacity:1}.stat-card{background:#1a1d20;border:1px solid #2d3238;border-radius:12px;overflow:hidden;transition:transform .2s ease,box-shadow .2s ease}.stat-card:hover{transform:translateY(-3px);box-shadow:0 10px 20px rgba(0,0,0,.3)!important;border-color:#0d6efd}.stat-icon{opacity:.6;font-size:1.2rem}.progress-stat{height:4px;background-color:#2b3035}.log-dark-input{background-color:#2b3035!important;border-color:#444b52!important;color:#e9ecef!important}.log-dark-input:focus{background-color:#32383e!important;border-color:#0d6efd!important;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.input-group-text-dark{background-color:#1a1d20!important;border-color:#444b52!important;color:#adb5bd!important}#logviewer::-webkit-scrollbar{width:8px}#logviewer::-webkit-scrollbar-track{background:#000}#logviewer::-webkit-scrollbar-thumb{background:#333;border-radius:4px}.download-wrapper{min-height:70vh;display:flex;align-items:center;justify-content:center}.file-card{border:1px solid #333;border-radius:1rem;box-shadow:0 10px 30px rgba(0,0,0,.5);max-width:450px;width:100%;background:#1e1e1e;color:#e0e0e0}.paste-card{border:1px solid #333;border-radius:1rem;box-shadow:0 10px 30px rgba(0,0,0,.5);min-width:300px;width:80%;background:#1e1e1e;color:#e0e0e0}@media(max-width:767px){.paste-card{width:100%;min-width:unset;border-radius:0;border-left:none;border-right:none;box-shadow:none}}.icon-container{width:70px;height:70px;background-color:#2d2d2d;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 1.5rem;color:#0d6efd}.filename-text{word-wrap:break-word;color:#fff;font-weight:600}.dark-list-item{background-color:#252525!important;border-color:#333!important;color:#b0b0b0!important}.popover{--bs-popover-bg:#212529;--bs-popover-header-bg:#212529;--bs-popover-header-color:#dee2e6;--bs-popover-body-color:#dee2e6;--bs-popover-body-padding-y:0;--bs-popover-border-color:rgba(255, 255, 255, 0.15);--bs-popover-arrow-border:rgba(255, 255, 255, 0.15)}.popover-header{border-bottom-color:rgba(255,255,255,.15)}.upload-options-section-label{font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:#6c757d;margin-bottom:.5rem}.upload-options-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:.5rem;margin-bottom:.75rem}@media(max-width:767px){.upload-options-grid{grid-template-columns:1fr}}.upload-option-card{background-color:#212529;border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:.5rem .75rem;transition:border-color .2s ease,box-shadow .2s ease}.upload-option-card:has(.upload-option-toggle:checked){border-color:rgba(13,110,253,.5);box-shadow:0 0 0 1px rgba(13,110,253,.2)}.upload-option-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem}.upload-option-icon{font-size:.85rem;color:#6ea8fe;opacity:.9}.upload-option-label{font-size:.72rem;font-weight:600;color:#adb5bd;text-transform:uppercase;letter-spacing:.06em}.upload-option-toggle{cursor:pointer}.upload-option-input-group{margin-top:0}.upload-option-card .form-control{background-color:#2b3035;border-color:rgba(255,255,255,.15);color:#dee2e6;padding:.25rem .5rem;font-size:.875rem}.upload-option-card .form-control:disabled{background-color:#262a2e;color:#555;border-color:rgba(255,255,255,8%)}.upload-option-suffix{background-color:#2b3035;border-color:rgba(255,255,255,.15);color:#adb5bd;font-size:.8rem;padding:.25rem .5rem}.upload-option-card input[type=number]::-webkit-inner-spin-button{filter:invert(1)brightness(.6)}.modal-samesize-input-edit{width:11rem}.modal-samesize-input-filerequest{width:7rem}.placeholder-white::placeholder{color:#6c757d}.filename{font-weight:700;font-size:14px;margin-bottom:5px}.upload-progress-container{display:flex;align-items:center}.upload-progress-bar{position:relative;height:10px;background-color:#eee;flex:1;margin-right:10px;border-radius:4px}.upload-progress-bar-progress{position:absolute;top:0;left:0;height:100%;background-color:#0a0;border-radius:4px;transition:width .2s ease-in-out}.upload-progress-info{font-size:12px}.us-container{margin-top:10px;margin-bottom:20px}.uploaderror{font-weight:700;color:red;margin-bottom:5px}.uploads-container{background-color:#2f343a;border:2px solid rgba(0,0,0,.3);border-radius:5px;margin-left:0;margin-right:0;max-width:none;visibility:hidden} \ No newline at end of file diff --git a/internal/webserver/web/static/js/admin_api.js b/internal/webserver/web/static/js/admin_api.js index 10e72b27..af674560 100644 --- a/internal/webserver/web/static/js/admin_api.js +++ b/internal/webserver/web/static/js/admin_api.js @@ -1026,3 +1026,56 @@ async function apiURequestSave(id, name, maxfiles, maxsize, expiry, notes) { throw error; } } + +// /files/addPaste + +async function apiAddPaste(content, title, allowedDownloads, expiryDays, password) { + const apiUrl = './api/files/addPaste'; + const reqPerm = 'PERM_UPLOAD'; + + let token; + try { + token = await getToken(reqPerm, false); + } catch (error) { + console.error("Unable to gain permission token:", error); + throw error; + } + + const formData = new FormData(); + formData.append("pasteContent", content); + if (title) { + formData.append("title", title); + } + formData.append("allowedDownloads", allowedDownloads); + formData.append("expiryDays", expiryDays); + if (password) { + formData.append("password", password); + } + + const requestOptions = { + method: 'POST', + headers: { + 'apikey': token, + }, + body: formData, + }; + + try { + const response = await fetch(apiUrl, requestOptions); + if (!response.ok) { + let errorMessage; + try { + const errorResponse = await response.json(); + errorMessage = errorResponse.ErrorMessage || `Request failed with status: ${response.status}`; + } catch { + const errorText = await response.text(); + errorMessage = errorText || `Request failed with status: ${response.status}`; + } + throw new Error(errorMessage); + } + return await response.json(); + } catch (error) { + console.error("Error in apiAddPaste:", error); + throw error; + } +} diff --git a/internal/webserver/web/static/js/admin_ui_paste.js b/internal/webserver/web/static/js/admin_ui_paste.js new file mode 100644 index 00000000..1e1603aa --- /dev/null +++ b/internal/webserver/web/static/js/admin_ui_paste.js @@ -0,0 +1,149 @@ +// This file contains JS code for the paste view. +// All files named admin_*.js will be merged together and minimised by calling +// go generate ./... + +function pasteToggleDownloads(checkbox) { + document.getElementById("paste-downloads").disabled = !checkbox.checked; +} + +function pasteToggleExpiry(checkbox) { + document.getElementById("paste-expiry").disabled = !checkbox.checked; +} + +function pasteTogglePassword(checkbox) { + document.getElementById("paste-password").disabled = !checkbox.checked; +} + +function submitPaste() { + const content = document.getElementById("paste-content").value.trim(); + if (!content) { + alert("Please enter some content before creating a paste."); + return; + } + + const title = document.getElementById("paste-title").value.trim(); + const limitViews = document.getElementById("paste-enable-downloads").checked; + const limitExpiry = document.getElementById("paste-enable-expiry").checked; + const usePassword = document.getElementById("paste-enable-password").checked; + + const allowedDownloads = limitViews ? parseInt(document.getElementById("paste-downloads").value, 10) : 0; + const expiryDays = limitExpiry ? parseInt(document.getElementById("paste-expiry").value, 10) : 0; + const password = usePassword ? document.getElementById("paste-password").value : ""; + + apiAddPaste(content, title, allowedDownloads, expiryDays, password) + .then(data => { + pasteInsertRow(data); + document.getElementById("paste-content").value = ""; + document.getElementById("paste-title").value = ""; + pasteCopyUrl(data.UrlDownload, data.Id); + }) + .catch(error => { + alert("Failed to create paste: " + error); + console.error("Error:", error); + }); +} + +function pasteInsertRow(info) { + const tbody = document.getElementById("paste-tbody"); + const row = document.createElement("tr"); + row.id = "pasterow-" + info.Id; + const viewsRemaining = info.UnlimitedDownloads ? "Unlimited" : info.DownloadsRemaining; + + const cells = [ + { text: info.Name }, + { id: `paste-created-${info.Id}` }, + { text: String(info.DownloadCount) }, + { id: `paste-expiry-${info.Id}` }, + { text: String(viewsRemaining) }, + ]; + + if (canViewOtherUploads) { + cells.push({ text: userNameSelf }); + } + + for (const cell of cells) { + const td = document.createElement("td"); + td.className = "newItem"; + if (cell.id) { + const span = document.createElement("span"); + span.id = cell.id; + td.appendChild(span); + } else { + td.textContent = cell.text; + } + row.appendChild(td); + } + + const actionsTd = document.createElement("td"); + actionsTd.className = "newItem"; + + const btnGroup = document.createElement("div"); + btnGroup.className = "btn-group"; + btnGroup.role = "group"; + + const copyBtn = document.createElement("button"); + copyBtn.type = "button"; + copyBtn.className = "btn btn-outline-light btn-sm"; + copyBtn.title = "Copy URL"; + copyBtn.addEventListener("click", () => pasteCopyUrl(info.UrlDownload, info.Id)); + const copyIcon = document.createElement("i"); + copyIcon.className = "bi bi-copy"; + copyBtn.appendChild(copyIcon); + + const deleteBtn = document.createElement("button"); + deleteBtn.type = "button"; + deleteBtn.className = "btn btn-outline-danger btn-sm"; + deleteBtn.title = "Delete"; + deleteBtn.addEventListener("click", () => pasteDelete(info.Id)); + const deleteIcon = document.createElement("i"); + deleteIcon.className = "bi bi-trash3"; + deleteBtn.appendChild(deleteIcon); + + btnGroup.appendChild(copyBtn); + btnGroup.appendChild(deleteBtn); + actionsTd.appendChild(btnGroup); + row.appendChild(actionsTd); + + tbody.prepend(row); + + insertDateWithNegative(info.UploadDate, `paste-created-${info.Id}`, "Unknown"); + if (info.UnlimitedTime) { + document.getElementById(`paste-expiry-${info.Id}`).innerText = "Never"; + } else { + insertFileRequestExpiry(info.ExpireAt, `paste-expiry-${info.Id}`); + } +} + +function pasteCopyUrl(url, id) { + navigator.clipboard.writeText(url).then(() => { + showToast(1000); + }).catch(() => { + }); +} + +function pasteDelete(id) { + if (!confirm("Delete this paste?")) { + return; + } + apiFilesDelete(id, 0) + .then(() => { + const row = document.getElementById("pasterow-" + id); + if (row) { + row.classList.add("rowDeleting"); + setTimeout(() => row.remove(), 290); + } + }) + .catch(error => { + alert("Failed to delete paste: " + error); + console.error("Error:", error); + }); +} + +function escapeHtml(str) { + return String(str) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} diff --git a/internal/webserver/web/static/js/min/admin.min.ccb62a8d5b.js b/internal/webserver/web/static/js/min/admin.min.ccb62a8d5b.js index 9f4f99c8..91dcf57e 100644 --- a/internal/webserver/web/static/js/min/admin.min.ccb62a8d5b.js +++ b/internal/webserver/web/static/js/min/admin.min.ccb62a8d5b.js @@ -1,6 +1,6 @@ -const storedTokens=new Map;async function getToken(e,t){const n="./auth/token";if(!t){if(!storedTokens.has(e))return getToken(e,!0);let t=storedTokens.get(e);return t.expiry-Date.now()/1e3<60?getToken(e,!0):t.key}const s={method:"POST",headers:{"Content-Type":"application/json",permission:e}};try{const o=await fetch(n,s);if(!o.ok)throw new Error(`Request failed with status: ${o.status}`);const t=await o.json();if(!t.hasOwnProperty("key"))throw new Error(`Invalid response when trying to get token`);return storedTokens.set(e,{key:t.key,expiry:t.expiry}),t.key}catch(e){throw console.error("Error in getToken:",e),e}}async function apiAuthModify(e,t,n){const o="./api/auth/modify",i="PERM_API_MOD";let s;try{s=await getToken(i,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const a={method:"POST",headers:{"Content-Type":"application/json",apikey:s,targetKey:e,permission:t,permissionModifier:n}};try{const e=await fetch(o,a);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiAuthModify:",e),e}}async function apiAuthFriendlyName(e,t){const s="./api/auth/friendlyname",o="PERM_API_MOD";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"PUT",headers:{"Content-Type":"application/json",apikey:n,targetKey:e,friendlyName:t}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiAuthModify:",e),e}}async function apiAuthDelete(e){const n="./api/auth/delete",s="PERM_API_MOD";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"POST",headers:{"Content-Type":"application/json",apikey:t,targetKey:e}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiAuthDelete:",e),e}}async function apiAuthCreate(){const t="./api/auth/create",n="PERM_API_MOD";let e;try{e=await getToken(n,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const s={method:"POST",headers:{"Content-Type":"application/json",apikey:e,basicPermissions:"true"}};try{const e=await fetch(t,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const n=await e.json();return n}catch(e){throw console.error("Error in apiAuthCreate:",e),e}}async function apiChunkComplete(e,t,n,s,o,i,a,r,c,l){const u="./api/chunk/complete",h="PERM_UPLOAD";let d;try{d=await getToken(h,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const m={method:"POST",headers:{"Content-Type":"application/json",apikey:d,uuid:e,filename:"base64:"+Base64.encode(t),filesize:n,realsize:s,contenttype:o,allowedDownloads:i,expiryDays:a,password:r,isE2E:c,nonblocking:l}};try{const e=await fetch(u,m);if(!e.ok){let t;try{const n=await e.json();t=n.ErrorMessage||`Request failed with status: ${e.status}`}catch{const n=await e.text();t=n||`Request failed with status: ${e.status}`}throw new Error(t)}const t=await e.json();return t}catch(e){throw console.error("Error in apiChunkComplete:",e),e}}async function apiFilesReplace(e,t){const s="./api/files/replace",o="PERM_REPLACE";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"PUT",headers:{"Content-Type":"application/json",id:e,apikey:n,idNewContent:t,deleteNewFile:!1}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesReplace:",e),e}}async function apiFilesListById(e){const n="./api/files/list/"+e,s="PERM_VIEW";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"GET",headers:{"Content-Type":"application/json",apikey:t}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesListById:",e),e}}async function apiFilesListDownloadSingle(e){const n="./api/files/download/"+e,s="PERM_DOWNLOAD";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"GET",headers:{"Content-Type":"application/json",apikey:t,presignUrl:!0}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesListDownloadSingle:",e),e}}async function apiFilesListDownloadZip(e,t){const s="./api/files/downloadzip",o="PERM_DOWNLOAD";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"GET",headers:{"Content-Type":"application/json",apikey:n,ids:e,filename:"base64:"+Base64.encode(t),presignUrl:!0}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesListDownloadZip:",e),e}}async function apiFilesModify(e,t,n,s,o){const a="./api/files/modify",r="PERM_EDIT";let i;try{i=await getToken(r,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const c={method:"PUT",headers:{"Content-Type":"application/json",id:e,apikey:i,allowedDownloads:t,expiryTimestamp:n,password:s,originalPassword:o}};try{const e=await fetch(a,c);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesModify:",e),e}}async function apiFilesDelete(e,t){const s="./api/files/delete",o="PERM_DELETE";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"POST",headers:{"Content-Type":"application/json",apikey:n,id:e,delay:t}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiFilesDelete:",e),e}}async function apiFilesRestore(e){const n="./api/files/restore",s="PERM_DELETE";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"POST",headers:{"Content-Type":"application/json",apikey:t,id:e}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesRestore:",e),e}}async function apiUserCreate(e){const n="./api/user/create",s="PERM_MANAGE_USERS";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"POST",headers:{"Content-Type":"application/json",apikey:t,username:e}};try{const e=await fetch(n,o);if(!e.ok)throw e.status==409?new Error("duplicate"):new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiUserModify:",e),e}}async function apiUserModify(e,t,n){const o="./api/user/modify",i="PERM_MANAGE_USERS";let s;try{s=await getToken(i,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const a={method:"POST",headers:{"Content-Type":"application/json",apikey:s,userid:e,userpermission:t,permissionModifier:n}};try{const e=await fetch(o,a);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiUserModify:",e),e}}async function apiUserChangeRank(e,t){const s="./api/user/changeRank",o="PERM_MANAGE_USERS";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"POST",headers:{"Content-Type":"application/json",apikey:n,userid:e,newRank:t}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiUserModify:",e),e}}async function apiUserDelete(e,t){const s="./api/user/delete",o="PERM_MANAGE_USERS";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"POST",headers:{"Content-Type":"application/json",apikey:n,userid:e,deleteFiles:t}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiUserDelete:",e),e}}async function apiUserResetPassword(e,t){const s="./api/user/resetPassword",o="PERM_MANAGE_USERS";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"POST",headers:{"Content-Type":"application/json",apikey:n,userid:e,generateNewPassword:t}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiUserResetPassword:",e),e}}async function apiLogSystemStatus(){const t="./api/logs/systemStatus",n="PERM_MANAGE_LOGS";let e;try{e=await getToken(n,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const s={method:"GET",headers:{"Content-Type":"application/json",apikey:e}};try{const e=await fetch(t,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const n=await e.json();return n}catch(e){throw console.error("Error in apiLogSystemStatus:",e),e}}async function apiLogResetTraffic(){const t="./api/logs/resetTraffic",n="PERM_MANAGE_LOGS";let e;try{e=await getToken(n,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const s={method:"GET",headers:{"Content-Type":"application/json",apikey:e}};try{const e=await fetch(t,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiLogResetTraffic:",e),e}}async function apiLogGet(e){const n="./api/logs/get",s="PERM_MANAGE_LOGS";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"GET",headers:{"Content-Type":"application/json",apikey:t,timestamp:e}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiLogGet:",e),e}}async function apiLogsDelete(e){const n="./api/logs/delete",s="PERM_MANAGE_LOGS";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"POST",headers:{"Content-Type":"application/json",apikey:t,timestamp:e}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiLogsDelete:",e),e}}async function apiE2eGet(){const t="./api/e2e/get",n="PERM_UPLOAD";let e;try{e=await getToken(n,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const s={method:"POST",headers:{"Content-Type":"application/json",apikey:e}};try{const e=await fetch(t,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);return await e.text()}catch(e){throw console.error("Error in apiE2eGet:",e),e}}async function apiE2eMutexLockUnlock(e){let t="./api/e2e/mutex/lock";e&&(t="./api/e2e/mutex/unlock");const s="PERM_UPLOAD";let n;try{n=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"GET",headers:{"Content-Type":"application/json",apikey:n}};try{const e=await fetch(t,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);return await e.text()}catch(e){throw console.error("Error in apiE2eMutexLock:",e),e}}async function apiE2eStore(e){const n="./api/e2e/set",s="PERM_UPLOAD";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"POST",headers:{"Content-Type":"application/json",apikey:t},body:JSON.stringify({content:e})};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiE2eStore:",e),e}}async function apiURequestDelete(e){const n="./api/uploadrequest/delete",s="PERM_MANAGE_FILE_REQUESTS";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"DELETE",headers:{"Content-Type":"application/json",apikey:t,id:e}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiURequestDelete:",e),e}}async function apiURequestSave(e,t,n,s,o,i){const r="./api/uploadrequest/save",c="PERM_MANAGE_FILE_REQUESTS";let a;try{a=await getToken(c,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const l={method:"POST",headers:{"Content-Type":"application/json",apikey:a,id:e,name:"base64:"+Base64.encode(t),expiry:o,maxfiles:n,maxsize:s,notes:"base64:"+Base64.encode(i)}};try{const e=await fetch(r,l);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiURequestDelete:",e),e}}try{var toastId,calendarInstance,dropzoneObject,isE2EEnabled,isUploading,rowCount,sseWorkerPort,statusItemCount,clipboard=new ClipboardJS(".copyurl")}catch{}function showToast(e,t){let n=document.getElementById("toastnotification");typeof t!="undefined"?n.innerText=t:n.innerText=n.dataset.default,n.classList.add("show"),clearTimeout(toastId),toastId=setTimeout(()=>{hideToast()},e)}function hideToast(){document.getElementById("toastnotification").classList.remove("show")}calendarInstance=null;function createCalendar(e,t){const n=new Date(t*1e3);calendarInstance=flatpickr(document.getElementById(e),{enableTime:!0,dateFormat:"U",altInput:!0,altFormat:"Y-m-d H:i",allowInput:!0,time_24hr:!0,defaultDate:n,minDate:"today"})}function handleEditCheckboxChange(e){var t=document.getElementById(e.getAttribute("data-toggle-target")),n=e.getAttribute("data-timestamp");e.checked?(t.classList.remove("disabled"),t.removeAttribute("disabled"),n!=null&&(calendarInstance._input.disabled=!1)):(n!=null&&(calendarInstance._input.disabled=!0),t.classList.add("disabled"),t.setAttribute("disabled",!0))}function downloadFileWithPresign(e){apiFilesListDownloadSingle(e).then(e=>{if(!e.hasOwnProperty("downloadUrl"))throw new Error("Unable to get presigned key");const t=document.createElement("a");t.href=e.downloadUrl,t.style.display="none",document.body.appendChild(t),t.click(),t.remove()}).catch(e=>{alert("Unable to download: "+e),console.error("Error:",e)})}function downloadFilesZipWithPresign(e,t){apiFilesListDownloadZip(e,t).then(e=>{if(!e.hasOwnProperty("downloadUrl"))throw new Error("Unable to get presigned key");const t=document.createElement("a");t.href=e.downloadUrl,t.style.display="none",document.body.appendChild(t),t.click(),t.remove()}).catch(e=>{alert("Unable to download: "+e),console.error("Error:",e)})}function doLogout(){typeof sseWorkerPort!="undefined"&&sseWorkerPort!==null&&sseWorkerPort.postMessage({type:"shutdown"}),window.location.href="./logout"}function changeApiPermission(e,t,n){var o,i,s=document.getElementById(n);if(s.classList.contains("perm-processing")||s.classList.contains("perm-nochange"))return;o=s.classList.contains("perm-granted"),s.classList.add("perm-processing"),s.classList.remove("perm-granted"),s.classList.remove("perm-notgranted"),i="GRANT",o&&(i="REVOKE"),apiAuthModify(e,t,i).then(e=>{o?(s.classList.add("perm-notgranted"),s.classList.add("perm-nownotgranted")):(s.classList.add("perm-granted"),s.classList.add("perm-nowgranted")),s.classList.remove("perm-processing"),setTimeout(()=>{s.classList.remove("perm-nowgranted"),s.classList.remove("perm-nownotgranted")},1e3)}).catch(e=>{o?s.classList.add("perm-granted"):s.classList.add("perm-notgranted"),s.classList.remove("perm-processing"),alert("Unable to set permission: "+e),console.error("Error:",e)})}function deleteApiKey(e){document.getElementById("delete-"+e).disabled=!0,apiAuthDelete(e).then(t=>{document.getElementById("row-"+e).classList.add("rowDeleting"),setTimeout(()=>{document.getElementById("row-"+e).remove()},290)}).catch(e=>{alert("Unable to delete API key: "+e),console.error("Error:",e)})}function newApiKey(){document.getElementById("button-newapi").disabled=!0,apiAuthCreate().then(e=>{addRowApi(e.Id,e.PublicId),document.getElementById("button-newapi").disabled=!1}).catch(e=>{alert("Unable to create API key: "+e),console.error("Error:",e)})}function addFriendlyNameChange(e){let t=document.getElementById("friendlyname-"+e);if(t.classList.contains("isBeingEdited"))return;t.classList.add("isBeingEdited");let i=t.innerText,n=document.createElement("input");n.size=5,n.value=i;let s=!0,o=function(){if(!s)return;s=!1;let o=n.value;o==""&&(o="Unnamed key"),t.innerText=o,t.classList.remove("isBeingEdited"),apiAuthFriendlyName(e,o).catch(e=>{alert("Unable to save name: "+e),console.error("Error:",e)})};n.onblur=o,n.addEventListener("keyup",function(e){e.keyCode===13&&(e.preventDefault(),o())}),t.innerText="",t.appendChild(n),n.focus()}function addRowApi(e,t){let p=document.getElementById("apitable"),s=p.insertRow(0);s.id="row-"+t;let i=0,c=s.insertCell(i++),l=s.insertCell(i++),d=s.insertCell(i++),a=s.insertCell(i++),u;canViewOtherApiKeys&&(u=s.insertCell(i++));let h=s.insertCell(i++);canViewOtherApiKeys&&(u.classList.add("newApiKey"),u.innerText=userName),c.classList.add("newApiKey"),l.classList.add("newApiKey"),d.classList.add("newApiKey"),a.classList.add("newApiKey"),a.classList.add("prevent-select"),h.classList.add("newApiKey"),c.innerText="Unnamed key",c.id="friendlyname-"+t,c.onclick=function(){addFriendlyNameChange(t)},l.innerText=e,l.classList.add("font-monospace"),d.innerText="Never";const r=document.createElement("div");r.className="btn-group",r.setAttribute("role","group");const n=document.createElement("button");n.type="button",n.dataset.clipboardText=e,n.title="Copy API Key",n.className="copyurl btn btn-outline-light btn-sm",n.setAttribute("onclick","showToast(1000)");const m=document.createElement("i");m.className="bi bi-copy",n.appendChild(m);const o=document.createElement("button");o.type="button",o.id=`delete-${t}`,o.title="Delete",o.className="btn btn-outline-danger btn-sm",o.setAttribute("onclick",`deleteApiKey('${t}')`);const f=document.createElement("i");f.className="bi bi-trash3",o.appendChild(f),r.appendChild(n),r.appendChild(o),h.appendChild(r);const g=[{perm:"PERM_VIEW",icon:"bi-eye",granted:!0,title:"List Uploads"},{perm:"PERM_UPLOAD",icon:"bi-file-earmark-plus",granted:!0,title:"Upload"},{perm:"PERM_EDIT",icon:"bi-pencil",granted:!0,title:"Edit Uploads"},{perm:"PERM_DELETE",icon:"bi-trash3",granted:!0,title:"Delete Uploads"},{perm:"PERM_REPLACE",icon:"bi-recycle",granted:!1,title:"Replace Uploads"},{perm:"PERM_DOWNLOAD",icon:"bi-box-arrow-in-down",granted:!1,title:"Download Files"},{perm:"PERM_MANAGE_FILE_REQUESTS",icon:"bi-file-earmark-arrow-up",granted:!1,title:"Manage File Requests"},{perm:"PERM_MANAGE_USERS",icon:"bi-people",granted:!1,title:"Manage Users"},{perm:"PERM_MANAGE_LOGS",icon:"bi-card-list",granted:!1,title:"Manage System Logs"},{perm:"PERM_API_MOD",icon:"bi-sliders2",granted:!1,title:"Manage API Keys"}];if(g.forEach(({perm:e,icon:n,granted:s,title:o})=>{const i=document.createElement("i"),r=`${e.toLowerCase()}_${t}`;i.id=r,i.className=`bi ${n} ${s?"perm-granted":"perm-notgranted"}`,i.title=o,i.setAttribute("onclick",`changeApiPermission("${t}","${e}", "${r}");`),a.appendChild(i),a.appendChild(document.createTextNode(" "))}),!canReplaceFiles){let e=document.getElementById("perm_replace_"+t);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}if(!canManageUsers){let e=document.getElementById("perm_manage_users_"+t);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}if(!canViewSystemLog){let e=document.getElementById("perm_manage_logs_"+t);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}if(!canCreateFileRequest){let e=document.getElementById("perm_manage_file_requests_"+t);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}setTimeout(()=>{c.classList.remove("newApiKey"),l.classList.remove("newApiKey"),d.classList.remove("newApiKey"),a.classList.remove("newApiKey"),h.classList.remove("newApiKey")},700)}function deleteFileRequest(e){document.getElementById("delete-"+e).disabled=!0,apiURequestDelete(e).then(t=>{const s=document.getElementById("row-"+e),n=document.getElementById("filelist-"+e);s.classList.add("rowDeleting"),n!==null&&n.classList.add("rowDeleting"),setTimeout(()=>{s.remove(),n!==null&&n.remove()},290)}).catch(e=>{alert("Unable to delete file request: "+e),console.error("Error:",e)})}function deleteOrShowModal(e,t,n){n===0?deleteFileRequest(e):showDeleteFRequestModal(e,t,n)}function deleteFileFr(e,t){document.getElementById("button-delete-"+e).disabled=!0;let n=document.getElementById("cell-listupload-"+e);apiFilesDelete(e,10).then(s=>{changeFileCountFr(t,-1),removeDownloadFileReference(e,t),n.classList.add("rowDeleting"),setTimeout(()=>{n.remove()},290),showToastFileDeletionFr(e)}).catch(e=>{alert("Unable to delete file: "+e),console.error("Error:",e)})}function changeFileCountFr(e,t){let n=document.getElementById("totalFiles-fr-"+e),s=Number(n.innerText)||0,o=s+t;n.innerText=o}function removeDownloadFileReference(e,t){const n=document.getElementById(`download-${t}`);if(!n)return;const a=n.getAttribute("onclick")||"",o=a.match(/downloadFilesZipWithPresign\('([^']*)',\s*'([^']*)'\)/),r=a.match(/downloadFileWithPresign\('([^']*)'\)/);let s=[],i="";o?(s=o[1].split(",").filter(e=>e!==""),i=o[2]):r&&(s=[r[1]],i=n.dataset.recordName||""),s=s.filter(t=>t!==e),s.length===0?(n.classList.add("disabled"),n.removeAttribute("onclick")):s.length===1?(n.classList.remove("disabled"),n.setAttribute("onclick",`downloadFileWithPresign('${s[0]}');`)):(n.classList.remove("disabled"),n.setAttribute("onclick",`downloadFilesZipWithPresign('${s.join(",")}', '${i}');`))}function showToastFileDeletionFr(e){let t=document.getElementById("toastnotificationUndo"),n=document.getElementById("cell-name-"+e).innerText,s=document.getElementById("toastFilename"),o=document.getElementById("toastUndoButton");s.innerText=n,o.dataset.fileid=e,hideToast(),t.classList.add("show"),clearTimeout(toastId),toastId=setTimeout(()=>{hideFileToast()},5e3)}function handleUndoFr(e){hideFileToast(),apiFilesRestore(e.dataset.fileid).then(e=>{window.location.reload()}).catch(e=>{alert("Unable to restore file: "+e),console.error("Error:",e)})}function showDeleteFRequestModal(e,t,n){document.getElementById("deleteModalBodyName").innerText=t,document.getElementById("deleteModalBodyCount").innerText=n,$("#deleteModal").modal("show"),document.getElementById("buttonDelete").onclick=function(){$("#deleteModal").modal("hide"),deleteFileRequest(e)}}function newFileRequest(){loadFileRequestDefaults(),document.getElementById("m_urequestlabel").innerText="New File Request",$("#addEditModal").modal("show"),document.getElementById("b_fr_save").onclick=function(){saveFileRequestDefaults(),saveFileRequest(),$("#addEditModal").modal("hide")}}function saveFileRequestDefaults(){if(document.getElementById("mc_maxfiles").checked?localStorage.setItem("fr_maxfiles",document.getElementById("mi_maxfiles").value):localStorage.setItem("fr_maxfiles",0),document.getElementById("mc_maxsize").checked?localStorage.setItem("fr_maxsize",document.getElementById("mi_maxsize").value):localStorage.setItem("fr_maxsize",0),document.getElementById("mc_expiry").checked){let e=document.getElementById("mi_expiry").value-Math.round(Date.now()/1e3);localStorage.setItem("fr_expiry",e)}else localStorage.setItem("fr_expiry",0)}function loadFileRequestDefaults(){const t=localStorage.getItem("fr_maxfiles"),n=localStorage.getItem("fr_maxsize");let e=localStorage.getItem("fr_expiry");if(e!=="0"&&e!==null){let t=new Date(Date.now()+Number(e*1e3));t.setHours(12,0,0,0),e=Math.floor(t.getTime()/1e3)}setModalValues("","",t,n,e,"")}function setModalValues(e,t,n,s,o,i){if(document.getElementById("freqId").value=e,t===null?document.getElementById("mFriendlyName").value="":document.getElementById("mFriendlyName").value=t,limitMaxFiles!=0){let e=document.getElementById("mc_maxfiles");(n===null||n==0)&&(n=limitMaxFiles),e.checked=!0,e.disabled=!0,e.title="Only admins can set this to unlimited",e.value="1",document.getElementById("mi_maxfiles").setAttribute("max",limitMaxFiles)}else{let e=document.getElementById("mc_maxfiles");e.disabled=!1,e.title="",document.getElementById("mi_maxfiles").setAttribute("max","")}if(limitMaxSize!=0){let e=document.getElementById("mc_maxsize");(s===null||s==0)&&(s=limitMaxSize),e.checked=!0,e.disabled=!0,e.title="Only admins can set this to unlimited",e.value="1",document.getElementById("mi_maxsize").setAttribute("max",limitMaxSize)}else{let e=document.getElementById("mc_maxsize");e.disabled=!1,e.title="",document.getElementById("mi_maxsize").setAttribute("max","")}if(n===null||n==0?(document.getElementById("mi_maxfiles").value="1",document.getElementById("mi_maxfiles").disabled=!0,document.getElementById("mc_maxfiles").checked=!1):(document.getElementById("mi_maxfiles").value=n,document.getElementById("mi_maxfiles").disabled=!1,document.getElementById("mc_maxfiles").checked=!0),s===null||s==0?(document.getElementById("mi_maxsize").value="10",document.getElementById("mi_maxsize").disabled=!0,document.getElementById("mc_maxsize").checked=!1):(document.getElementById("mi_maxsize").value=s,document.getElementById("mi_maxsize").disabled=!1,document.getElementById("mc_maxsize").checked=!0),o===null||o==0){const e=Math.floor(new Date(Date.now()+14*24*60*60*1e3).getTime()/1e3);document.getElementById("mi_expiry").disabled=!0,document.getElementById("mc_expiry").checked=!1,document.getElementById("mi_expiry").value=e,createCalendar("mi_expiry",e)}else document.getElementById("mi_expiry").value=o,document.getElementById("mi_expiry").disabled=!1,document.getElementById("mc_expiry").checked=!0,createCalendar("mi_expiry",o);document.getElementById("mNotes").value=i}function editFileRequest(e,t,n,s,o,i){setModalValues(e,t,n,s,o,i),document.getElementById("m_urequestlabel").innerText="Edit File Request",$("#addEditModal").modal("show"),document.getElementById("b_fr_save").onclick=function(){saveFileRequest(),$("#addEditModal").modal("hide")}}function saveFileRequest(){const s=document.getElementById("b_fr_save"),o=document.getElementById("freqId").value,i=document.getElementById("mFriendlyName").value,a=document.getElementById("mNotes").value;let e=0,t=0,n=0;document.getElementById("mc_maxfiles").checked&&(e=document.getElementById("mi_maxfiles").value),document.getElementById("mc_maxsize").checked&&(t=document.getElementById("mi_maxsize").value),document.getElementById("mc_expiry").checked&&(n=document.getElementById("mi_expiry").value),s.disabled=!0,apiURequestSave(o,i,e,t,n,a).then(e=>{document.getElementById("b_fr_save").disabled=!1,insertOrReplaceFileRequest(e)}).catch(e=>{alert("Unable to save file request: "+e),console.error("Error:",e),document.getElementById("b_fr_save").disabled=!1})}function checkMaxNumber(e){if(e.value==""){e.value="1";return}let t=e.getAttribute("max");if(t=="")return;e.value>t&&(e.value=t)}function insertOrReplaceFileRequest(e){const n=document.getElementById("filerequesttable");let t=document.getElementById(`row-${e.id}`);if(t){const n=document.getElementById(`cell-username-${e.id}`).innerText;t.replaceWith(createFileRequestRow(e,n))}else{let t=createFileRequestRow(e,userName);t.querySelectorAll("td").forEach(e=>{e.classList.add("newFileRequest"),setTimeout(()=>{e.classList.remove("newFileRequest")},700)}),n.prepend(t)}}function createFileRequestRow(e,t){function r(e){const t=document.createElement("td");return t.textContent=e,t}function h(e,t){const s=document.createElement("td"),n=document.createElement("a");return n.textContent=e,n.href=t,n.target="_blank",s.appendChild(n),s}function c(e){const t=document.createElement("i");return t.className=`bi ${e}`,t}const d=`${baseUrl}publicUpload?id=${e.id}&key=${e.apikey}`,n=document.createElement("tr");if(n.id=`row-${e.id}`,n.className="filerequest-item",n.appendChild(h(e.name,d)),e.maxfiles==0?n.appendChild(r(e.uploadedfiles)):n.appendChild(r(`${e.uploadedfiles} / ${e.maxfiles}`)),n.appendChild(r(getReadableSize(e.totalfilesize))),n.appendChild(r(formatTimestampWithNegative(e.lastupload,"None"))),n.appendChild(r(formatFileRequestExpiry(e.expiry))),canViewOtherRequests){let s=r(t);s.id=`cell-username-${e.id}`,n.appendChild(s)}const u=document.createElement("td"),l=document.createElement("div");l.className="btn-group",l.role="group";const o=document.createElement("button");o.id=`download-${e.id}`,o.type="button",o.className="btn btn-outline-light btn-sm",o.title="Download all",e.uploadedfiles==0&&o.classList.add("disabled"),o.appendChild(c("bi-download"));const s=document.createElement("button");s.id=`copy-${e.id}`,s.type="button",s.className="copyurl btn btn-outline-light btn-sm",s.title="Copy URL",s.setAttribute("data-clipboard-text",d),s.onclick=()=>showToast(1e3),s.appendChild(c("bi-copy"));const i=document.createElement("button");i.id=`edit-${e.id}`,i.type="button",i.className="btn btn-outline-light btn-sm",i.title="Edit request",i.onclick=()=>editFileRequest(e.id,e.name,e.maxfiles,e.maxsize,e.expiry,e.notes),i.appendChild(c("bi-pencil"));const a=document.createElement("button");return a.id=`delete-${e.id}`,a.type="button",a.className="btn btn-outline-danger btn-sm",a.title="Delete",a.onclick=()=>deleteOrShowModal(e.id,e.name,e.uploadedfiles),a.appendChild(c("bi-trash3")),l.append(o,s,i,a),u.appendChild(l),n.appendChild(u),n}function filterLogs(e){const t=document.getElementById("logviewer");e=="all"?t.value=logContent:t.value=logContent.split(` +const storedTokens=new Map;async function getToken(e,t){const n="./auth/token";if(!t){if(!storedTokens.has(e))return getToken(e,!0);let t=storedTokens.get(e);return t.expiry-Date.now()/1e3<60?getToken(e,!0):t.key}const s={method:"POST",headers:{"Content-Type":"application/json",permission:e}};try{const o=await fetch(n,s);if(!o.ok)throw new Error(`Request failed with status: ${o.status}`);const t=await o.json();if(!t.hasOwnProperty("key"))throw new Error(`Invalid response when trying to get token`);return storedTokens.set(e,{key:t.key,expiry:t.expiry}),t.key}catch(e){throw console.error("Error in getToken:",e),e}}async function apiAuthModify(e,t,n){const o="./api/auth/modify",i="PERM_API_MOD";let s;try{s=await getToken(i,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const a={method:"POST",headers:{"Content-Type":"application/json",apikey:s,targetKey:e,permission:t,permissionModifier:n}};try{const e=await fetch(o,a);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiAuthModify:",e),e}}async function apiAuthFriendlyName(e,t){const s="./api/auth/friendlyname",o="PERM_API_MOD";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"PUT",headers:{"Content-Type":"application/json",apikey:n,targetKey:e,friendlyName:t}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiAuthModify:",e),e}}async function apiAuthDelete(e){const n="./api/auth/delete",s="PERM_API_MOD";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"POST",headers:{"Content-Type":"application/json",apikey:t,targetKey:e}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiAuthDelete:",e),e}}async function apiAuthCreate(){const t="./api/auth/create",n="PERM_API_MOD";let e;try{e=await getToken(n,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const s={method:"POST",headers:{"Content-Type":"application/json",apikey:e,basicPermissions:"true"}};try{const e=await fetch(t,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const n=await e.json();return n}catch(e){throw console.error("Error in apiAuthCreate:",e),e}}async function apiChunkComplete(e,t,n,s,o,i,a,r,c,l){const u="./api/chunk/complete",h="PERM_UPLOAD";let d;try{d=await getToken(h,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const m={method:"POST",headers:{"Content-Type":"application/json",apikey:d,uuid:e,filename:"base64:"+Base64.encode(t),filesize:n,realsize:s,contenttype:o,allowedDownloads:i,expiryDays:a,password:r,isE2E:c,nonblocking:l}};try{const e=await fetch(u,m);if(!e.ok){let t;try{const n=await e.json();t=n.ErrorMessage||`Request failed with status: ${e.status}`}catch{const n=await e.text();t=n||`Request failed with status: ${e.status}`}throw new Error(t)}const t=await e.json();return t}catch(e){throw console.error("Error in apiChunkComplete:",e),e}}async function apiFilesReplace(e,t){const s="./api/files/replace",o="PERM_REPLACE";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"PUT",headers:{"Content-Type":"application/json",id:e,apikey:n,idNewContent:t,deleteNewFile:!1}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesReplace:",e),e}}async function apiFilesListById(e){const n="./api/files/list/"+e,s="PERM_VIEW";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"GET",headers:{"Content-Type":"application/json",apikey:t}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesListById:",e),e}}async function apiFilesListDownloadSingle(e){const n="./api/files/download/"+e,s="PERM_DOWNLOAD";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"GET",headers:{"Content-Type":"application/json",apikey:t,presignUrl:!0}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesListDownloadSingle:",e),e}}async function apiFilesListDownloadZip(e,t){const s="./api/files/downloadzip",o="PERM_DOWNLOAD";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"GET",headers:{"Content-Type":"application/json",apikey:n,ids:e,filename:"base64:"+Base64.encode(t),presignUrl:!0}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesListDownloadZip:",e),e}}async function apiFilesModify(e,t,n,s,o){const a="./api/files/modify",r="PERM_EDIT";let i;try{i=await getToken(r,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const c={method:"PUT",headers:{"Content-Type":"application/json",id:e,apikey:i,allowedDownloads:t,expiryTimestamp:n,password:s,originalPassword:o}};try{const e=await fetch(a,c);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesModify:",e),e}}async function apiFilesDelete(e,t){const s="./api/files/delete",o="PERM_DELETE";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"POST",headers:{"Content-Type":"application/json",apikey:n,id:e,delay:t}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiFilesDelete:",e),e}}async function apiFilesRestore(e){const n="./api/files/restore",s="PERM_DELETE";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"POST",headers:{"Content-Type":"application/json",apikey:t,id:e}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiFilesRestore:",e),e}}async function apiUserCreate(e){const n="./api/user/create",s="PERM_MANAGE_USERS";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"POST",headers:{"Content-Type":"application/json",apikey:t,username:e}};try{const e=await fetch(n,o);if(!e.ok)throw e.status==409?new Error("duplicate"):new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiUserModify:",e),e}}async function apiUserModify(e,t,n){const o="./api/user/modify",i="PERM_MANAGE_USERS";let s;try{s=await getToken(i,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const a={method:"POST",headers:{"Content-Type":"application/json",apikey:s,userid:e,userpermission:t,permissionModifier:n}};try{const e=await fetch(o,a);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiUserModify:",e),e}}async function apiUserChangeRank(e,t){const s="./api/user/changeRank",o="PERM_MANAGE_USERS";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"POST",headers:{"Content-Type":"application/json",apikey:n,userid:e,newRank:t}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiUserModify:",e),e}}async function apiUserDelete(e,t){const s="./api/user/delete",o="PERM_MANAGE_USERS";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"POST",headers:{"Content-Type":"application/json",apikey:n,userid:e,deleteFiles:t}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiUserDelete:",e),e}}async function apiUserResetPassword(e,t){const s="./api/user/resetPassword",o="PERM_MANAGE_USERS";let n;try{n=await getToken(o,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i={method:"POST",headers:{"Content-Type":"application/json",apikey:n,userid:e,generateNewPassword:t}};try{const e=await fetch(s,i);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiUserResetPassword:",e),e}}async function apiLogSystemStatus(){const t="./api/logs/systemStatus",n="PERM_MANAGE_LOGS";let e;try{e=await getToken(n,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const s={method:"GET",headers:{"Content-Type":"application/json",apikey:e}};try{const e=await fetch(t,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const n=await e.json();return n}catch(e){throw console.error("Error in apiLogSystemStatus:",e),e}}async function apiLogResetTraffic(){const t="./api/logs/resetTraffic",n="PERM_MANAGE_LOGS";let e;try{e=await getToken(n,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const s={method:"GET",headers:{"Content-Type":"application/json",apikey:e}};try{const e=await fetch(t,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiLogResetTraffic:",e),e}}async function apiLogGet(e){const n="./api/logs/get",s="PERM_MANAGE_LOGS";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"GET",headers:{"Content-Type":"application/json",apikey:t,timestamp:e}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiLogGet:",e),e}}async function apiLogsDelete(e){const n="./api/logs/delete",s="PERM_MANAGE_LOGS";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"POST",headers:{"Content-Type":"application/json",apikey:t,timestamp:e}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiLogsDelete:",e),e}}async function apiE2eGet(){const t="./api/e2e/get",n="PERM_UPLOAD";let e;try{e=await getToken(n,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const s={method:"POST",headers:{"Content-Type":"application/json",apikey:e}};try{const e=await fetch(t,s);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);return await e.text()}catch(e){throw console.error("Error in apiE2eGet:",e),e}}async function apiE2eMutexLockUnlock(e){let t="./api/e2e/mutex/lock";e&&(t="./api/e2e/mutex/unlock");const s="PERM_UPLOAD";let n;try{n=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"GET",headers:{"Content-Type":"application/json",apikey:n}};try{const e=await fetch(t,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);return await e.text()}catch(e){throw console.error("Error in apiE2eMutexLock:",e),e}}async function apiE2eStore(e){const n="./api/e2e/set",s="PERM_UPLOAD";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"POST",headers:{"Content-Type":"application/json",apikey:t},body:JSON.stringify({content:e})};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiE2eStore:",e),e}}async function apiURequestDelete(e){const n="./api/uploadrequest/delete",s="PERM_MANAGE_FILE_REQUESTS";let t;try{t=await getToken(s,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const o={method:"DELETE",headers:{"Content-Type":"application/json",apikey:t,id:e}};try{const e=await fetch(n,o);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`)}catch(e){throw console.error("Error in apiURequestDelete:",e),e}}async function apiURequestSave(e,t,n,s,o,i){const r="./api/uploadrequest/save",c="PERM_MANAGE_FILE_REQUESTS";let a;try{a=await getToken(c,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const l={method:"POST",headers:{"Content-Type":"application/json",apikey:a,id:e,name:"base64:"+Base64.encode(t),expiry:o,maxfiles:n,maxsize:s,notes:"base64:"+Base64.encode(i)}};try{const e=await fetch(r,l);if(!e.ok)throw new Error(`Request failed with status: ${e.status}`);const t=await e.json();return t}catch(e){throw console.error("Error in apiURequestDelete:",e),e}}async function apiAddPaste(e,t,n,s,o){const r="./api/files/addPaste",c="PERM_UPLOAD";let a;try{a=await getToken(c,!1)}catch(e){throw console.error("Unable to gain permission token:",e),e}const i=new FormData;i.append("pasteContent",e),t&&i.append("title",t),i.append("allowedDownloads",n),i.append("expiryDays",s),o&&i.append("password",o);const l={method:"POST",headers:{apikey:a},body:i};try{const e=await fetch(r,l);if(!e.ok){let t;try{const n=await e.json();t=n.ErrorMessage||`Request failed with status: ${e.status}`}catch{const n=await e.text();t=n||`Request failed with status: ${e.status}`}throw new Error(t)}return await e.json()}catch(e){throw console.error("Error in apiAddPaste:",e),e}}try{var toastId,calendarInstance,dropzoneObject,isE2EEnabled,isUploading,rowCount,sseWorkerPort,statusItemCount,clipboard=new ClipboardJS(".copyurl")}catch{}function showToast(e,t){let n=document.getElementById("toastnotification");typeof t!="undefined"?n.innerText=t:n.innerText=n.dataset.default,n.classList.add("show"),clearTimeout(toastId),toastId=setTimeout(()=>{hideToast()},e)}function hideToast(){document.getElementById("toastnotification").classList.remove("show")}calendarInstance=null;function createCalendar(e,t){const n=new Date(t*1e3);calendarInstance=flatpickr(document.getElementById(e),{enableTime:!0,dateFormat:"U",altInput:!0,altFormat:"Y-m-d H:i",allowInput:!0,time_24hr:!0,defaultDate:n,minDate:"today"})}function handleEditCheckboxChange(e){var t=document.getElementById(e.getAttribute("data-toggle-target")),n=e.getAttribute("data-timestamp");e.checked?(t.classList.remove("disabled"),t.removeAttribute("disabled"),n!=null&&(calendarInstance._input.disabled=!1)):(n!=null&&(calendarInstance._input.disabled=!0),t.classList.add("disabled"),t.setAttribute("disabled",!0))}function downloadFileWithPresign(e){apiFilesListDownloadSingle(e).then(e=>{if(!e.hasOwnProperty("downloadUrl"))throw new Error("Unable to get presigned key");const t=document.createElement("a");t.href=e.downloadUrl,t.style.display="none",document.body.appendChild(t),t.click(),t.remove()}).catch(e=>{alert("Unable to download: "+e),console.error("Error:",e)})}function downloadFilesZipWithPresign(e,t){apiFilesListDownloadZip(e,t).then(e=>{if(!e.hasOwnProperty("downloadUrl"))throw new Error("Unable to get presigned key");const t=document.createElement("a");t.href=e.downloadUrl,t.style.display="none",document.body.appendChild(t),t.click(),t.remove()}).catch(e=>{alert("Unable to download: "+e),console.error("Error:",e)})}function doLogout(){typeof sseWorkerPort!="undefined"&&sseWorkerPort!==null&&sseWorkerPort.postMessage({type:"shutdown"}),window.location.href="./logout"}function changeApiPermission(e,t,n){var o,i,s=document.getElementById(n);if(s.classList.contains("perm-processing")||s.classList.contains("perm-nochange"))return;o=s.classList.contains("perm-granted"),s.classList.add("perm-processing"),s.classList.remove("perm-granted"),s.classList.remove("perm-notgranted"),i="GRANT",o&&(i="REVOKE"),apiAuthModify(e,t,i).then(e=>{o?(s.classList.add("perm-notgranted"),s.classList.add("perm-nownotgranted")):(s.classList.add("perm-granted"),s.classList.add("perm-nowgranted")),s.classList.remove("perm-processing"),setTimeout(()=>{s.classList.remove("perm-nowgranted"),s.classList.remove("perm-nownotgranted")},1e3)}).catch(e=>{o?s.classList.add("perm-granted"):s.classList.add("perm-notgranted"),s.classList.remove("perm-processing"),alert("Unable to set permission: "+e),console.error("Error:",e)})}function deleteApiKey(e){document.getElementById("delete-"+e).disabled=!0,apiAuthDelete(e).then(t=>{document.getElementById("row-"+e).classList.add("rowDeleting"),setTimeout(()=>{document.getElementById("row-"+e).remove()},290)}).catch(e=>{alert("Unable to delete API key: "+e),console.error("Error:",e)})}function newApiKey(){document.getElementById("button-newapi").disabled=!0,apiAuthCreate().then(e=>{addRowApi(e.Id,e.PublicId),document.getElementById("button-newapi").disabled=!1}).catch(e=>{alert("Unable to create API key: "+e),console.error("Error:",e)})}function addFriendlyNameChange(e){let t=document.getElementById("friendlyname-"+e);if(t.classList.contains("isBeingEdited"))return;t.classList.add("isBeingEdited");let i=t.innerText,n=document.createElement("input");n.size=5,n.value=i;let s=!0,o=function(){if(!s)return;s=!1;let o=n.value;o==""&&(o="Unnamed key"),t.innerText=o,t.classList.remove("isBeingEdited"),apiAuthFriendlyName(e,o).catch(e=>{alert("Unable to save name: "+e),console.error("Error:",e)})};n.onblur=o,n.addEventListener("keyup",function(e){e.keyCode===13&&(e.preventDefault(),o())}),t.innerText="",t.appendChild(n),n.focus()}function addRowApi(e,t){let p=document.getElementById("apitable"),s=p.insertRow(0);s.id="row-"+t;let i=0,c=s.insertCell(i++),l=s.insertCell(i++),d=s.insertCell(i++),a=s.insertCell(i++),u;canViewOtherApiKeys&&(u=s.insertCell(i++));let h=s.insertCell(i++);canViewOtherApiKeys&&(u.classList.add("newApiKey"),u.innerText=userName),c.classList.add("newApiKey"),l.classList.add("newApiKey"),d.classList.add("newApiKey"),a.classList.add("newApiKey"),a.classList.add("prevent-select"),h.classList.add("newApiKey"),c.innerText="Unnamed key",c.id="friendlyname-"+t,c.onclick=function(){addFriendlyNameChange(t)},l.innerText=e,l.classList.add("font-monospace"),d.innerText="Never";const r=document.createElement("div");r.className="btn-group",r.setAttribute("role","group");const n=document.createElement("button");n.type="button",n.dataset.clipboardText=e,n.title="Copy API Key",n.className="copyurl btn btn-outline-light btn-sm",n.setAttribute("onclick","showToast(1000)");const m=document.createElement("i");m.className="bi bi-copy",n.appendChild(m);const o=document.createElement("button");o.type="button",o.id=`delete-${t}`,o.title="Delete",o.className="btn btn-outline-danger btn-sm",o.setAttribute("onclick",`deleteApiKey('${t}')`);const f=document.createElement("i");f.className="bi bi-trash3",o.appendChild(f),r.appendChild(n),r.appendChild(o),h.appendChild(r);const g=[{perm:"PERM_VIEW",icon:"bi-eye",granted:!0,title:"List Uploads"},{perm:"PERM_UPLOAD",icon:"bi-file-earmark-plus",granted:!0,title:"Upload"},{perm:"PERM_EDIT",icon:"bi-pencil",granted:!0,title:"Edit Uploads"},{perm:"PERM_DELETE",icon:"bi-trash3",granted:!0,title:"Delete Uploads"},{perm:"PERM_REPLACE",icon:"bi-recycle",granted:!1,title:"Replace Uploads"},{perm:"PERM_DOWNLOAD",icon:"bi-box-arrow-in-down",granted:!1,title:"Download Files"},{perm:"PERM_MANAGE_FILE_REQUESTS",icon:"bi-file-earmark-arrow-up",granted:!1,title:"Manage File Requests"},{perm:"PERM_MANAGE_USERS",icon:"bi-people",granted:!1,title:"Manage Users"},{perm:"PERM_MANAGE_LOGS",icon:"bi-card-list",granted:!1,title:"Manage System Logs"},{perm:"PERM_API_MOD",icon:"bi-sliders2",granted:!1,title:"Manage API Keys"}];if(g.forEach(({perm:e,icon:n,granted:s,title:o})=>{const i=document.createElement("i"),r=`${e.toLowerCase()}_${t}`;i.id=r,i.className=`bi ${n} ${s?"perm-granted":"perm-notgranted"}`,i.title=o,i.setAttribute("onclick",`changeApiPermission("${t}","${e}", "${r}");`),a.appendChild(i),a.appendChild(document.createTextNode(" "))}),!canReplaceFiles){let e=document.getElementById("perm_replace_"+t);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}if(!canManageUsers){let e=document.getElementById("perm_manage_users_"+t);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}if(!canViewSystemLog){let e=document.getElementById("perm_manage_logs_"+t);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}if(!canCreateFileRequest){let e=document.getElementById("perm_manage_file_requests_"+t);e.classList.add("perm-unavailable"),e.classList.add("perm-nochange")}setTimeout(()=>{c.classList.remove("newApiKey"),l.classList.remove("newApiKey"),d.classList.remove("newApiKey"),a.classList.remove("newApiKey"),h.classList.remove("newApiKey")},700)}function deleteFileRequest(e){document.getElementById("delete-"+e).disabled=!0,apiURequestDelete(e).then(t=>{const s=document.getElementById("row-"+e),n=document.getElementById("filelist-"+e);s.classList.add("rowDeleting"),n!==null&&n.classList.add("rowDeleting"),setTimeout(()=>{s.remove(),n!==null&&n.remove()},290)}).catch(e=>{alert("Unable to delete file request: "+e),console.error("Error:",e)})}function deleteOrShowModal(e,t,n){n===0?deleteFileRequest(e):showDeleteFRequestModal(e,t,n)}function deleteFileFr(e,t){document.getElementById("button-delete-"+e).disabled=!0;let n=document.getElementById("cell-listupload-"+e);apiFilesDelete(e,10).then(s=>{changeFileCountFr(t,-1),removeDownloadFileReference(e,t),n.classList.add("rowDeleting"),setTimeout(()=>{n.remove()},290),showToastFileDeletionFr(e)}).catch(e=>{alert("Unable to delete file: "+e),console.error("Error:",e)})}function changeFileCountFr(e,t){let n=document.getElementById("totalFiles-fr-"+e),s=Number(n.innerText)||0,o=s+t;n.innerText=o}function removeDownloadFileReference(e,t){const n=document.getElementById(`download-${t}`);if(!n)return;const a=n.getAttribute("onclick")||"",o=a.match(/downloadFilesZipWithPresign\('([^']*)',\s*'([^']*)'\)/),r=a.match(/downloadFileWithPresign\('([^']*)'\)/);let s=[],i="";o?(s=o[1].split(",").filter(e=>e!==""),i=o[2]):r&&(s=[r[1]],i=n.dataset.recordName||""),s=s.filter(t=>t!==e),s.length===0?(n.classList.add("disabled"),n.removeAttribute("onclick")):s.length===1?(n.classList.remove("disabled"),n.setAttribute("onclick",`downloadFileWithPresign('${s[0]}');`)):(n.classList.remove("disabled"),n.setAttribute("onclick",`downloadFilesZipWithPresign('${s.join(",")}', '${i}');`))}function showToastFileDeletionFr(e){let t=document.getElementById("toastnotificationUndo"),n=document.getElementById("cell-name-"+e).innerText,s=document.getElementById("toastFilename"),o=document.getElementById("toastUndoButton");s.innerText=n,o.dataset.fileid=e,hideToast(),t.classList.add("show"),clearTimeout(toastId),toastId=setTimeout(()=>{hideFileToast()},5e3)}function handleUndoFr(e){hideFileToast(),apiFilesRestore(e.dataset.fileid).then(e=>{window.location.reload()}).catch(e=>{alert("Unable to restore file: "+e),console.error("Error:",e)})}function showDeleteFRequestModal(e,t,n){document.getElementById("deleteModalBodyName").innerText=t,document.getElementById("deleteModalBodyCount").innerText=n,$("#deleteModal").modal("show"),document.getElementById("buttonDelete").onclick=function(){$("#deleteModal").modal("hide"),deleteFileRequest(e)}}function newFileRequest(){loadFileRequestDefaults(),document.getElementById("m_urequestlabel").innerText="New File Request",$("#addEditModal").modal("show"),document.getElementById("b_fr_save").onclick=function(){saveFileRequestDefaults(),saveFileRequest(),$("#addEditModal").modal("hide")}}function saveFileRequestDefaults(){if(document.getElementById("mc_maxfiles").checked?localStorage.setItem("fr_maxfiles",document.getElementById("mi_maxfiles").value):localStorage.setItem("fr_maxfiles",0),document.getElementById("mc_maxsize").checked?localStorage.setItem("fr_maxsize",document.getElementById("mi_maxsize").value):localStorage.setItem("fr_maxsize",0),document.getElementById("mc_expiry").checked){let e=document.getElementById("mi_expiry").value-Math.round(Date.now()/1e3);localStorage.setItem("fr_expiry",e)}else localStorage.setItem("fr_expiry",0)}function loadFileRequestDefaults(){const t=localStorage.getItem("fr_maxfiles"),n=localStorage.getItem("fr_maxsize");let e=localStorage.getItem("fr_expiry");if(e!=="0"&&e!==null){let t=new Date(Date.now()+Number(e*1e3));t.setHours(12,0,0,0),e=Math.floor(t.getTime()/1e3)}setModalValues("","",t,n,e,"")}function setModalValues(e,t,n,s,o,i){if(document.getElementById("freqId").value=e,t===null?document.getElementById("mFriendlyName").value="":document.getElementById("mFriendlyName").value=t,limitMaxFiles!=0){let e=document.getElementById("mc_maxfiles");(n===null||n==0)&&(n=limitMaxFiles),e.checked=!0,e.disabled=!0,e.title="Only admins can set this to unlimited",e.value="1",document.getElementById("mi_maxfiles").setAttribute("max",limitMaxFiles)}else{let e=document.getElementById("mc_maxfiles");e.disabled=!1,e.title="",document.getElementById("mi_maxfiles").setAttribute("max","")}if(limitMaxSize!=0){let e=document.getElementById("mc_maxsize");(s===null||s==0)&&(s=limitMaxSize),e.checked=!0,e.disabled=!0,e.title="Only admins can set this to unlimited",e.value="1",document.getElementById("mi_maxsize").setAttribute("max",limitMaxSize)}else{let e=document.getElementById("mc_maxsize");e.disabled=!1,e.title="",document.getElementById("mi_maxsize").setAttribute("max","")}if(n===null||n==0?(document.getElementById("mi_maxfiles").value="1",document.getElementById("mi_maxfiles").disabled=!0,document.getElementById("mc_maxfiles").checked=!1):(document.getElementById("mi_maxfiles").value=n,document.getElementById("mi_maxfiles").disabled=!1,document.getElementById("mc_maxfiles").checked=!0),s===null||s==0?(document.getElementById("mi_maxsize").value="10",document.getElementById("mi_maxsize").disabled=!0,document.getElementById("mc_maxsize").checked=!1):(document.getElementById("mi_maxsize").value=s,document.getElementById("mi_maxsize").disabled=!1,document.getElementById("mc_maxsize").checked=!0),o===null||o==0){const e=Math.floor(new Date(Date.now()+14*24*60*60*1e3).getTime()/1e3);document.getElementById("mi_expiry").disabled=!0,document.getElementById("mc_expiry").checked=!1,document.getElementById("mi_expiry").value=e,createCalendar("mi_expiry",e)}else document.getElementById("mi_expiry").value=o,document.getElementById("mi_expiry").disabled=!1,document.getElementById("mc_expiry").checked=!0,createCalendar("mi_expiry",o);document.getElementById("mNotes").value=i}function editFileRequest(e,t,n,s,o,i){setModalValues(e,t,n,s,o,i),document.getElementById("m_urequestlabel").innerText="Edit File Request",$("#addEditModal").modal("show"),document.getElementById("b_fr_save").onclick=function(){saveFileRequest(),$("#addEditModal").modal("hide")}}function saveFileRequest(){const s=document.getElementById("b_fr_save"),o=document.getElementById("freqId").value,i=document.getElementById("mFriendlyName").value,a=document.getElementById("mNotes").value;let e=0,t=0,n=0;document.getElementById("mc_maxfiles").checked&&(e=document.getElementById("mi_maxfiles").value),document.getElementById("mc_maxsize").checked&&(t=document.getElementById("mi_maxsize").value),document.getElementById("mc_expiry").checked&&(n=document.getElementById("mi_expiry").value),s.disabled=!0,apiURequestSave(o,i,e,t,n,a).then(e=>{document.getElementById("b_fr_save").disabled=!1,insertOrReplaceFileRequest(e)}).catch(e=>{alert("Unable to save file request: "+e),console.error("Error:",e),document.getElementById("b_fr_save").disabled=!1})}function checkMaxNumber(e){if(e.value==""){e.value="1";return}let t=e.getAttribute("max");if(t=="")return;e.value>t&&(e.value=t)}function insertOrReplaceFileRequest(e){const n=document.getElementById("filerequesttable");let t=document.getElementById(`row-${e.id}`);if(t){const n=document.getElementById(`cell-username-${e.id}`).innerText;t.replaceWith(createFileRequestRow(e,n))}else{let t=createFileRequestRow(e,userName);t.querySelectorAll("td").forEach(e=>{e.classList.add("newFileRequest"),setTimeout(()=>{e.classList.remove("newFileRequest")},700)}),n.prepend(t)}}function createFileRequestRow(e,t){function r(e){const t=document.createElement("td");return t.textContent=e,t}function h(e,t){const s=document.createElement("td"),n=document.createElement("a");return n.textContent=e,n.href=t,n.target="_blank",s.appendChild(n),s}function c(e){const t=document.createElement("i");return t.className=`bi ${e}`,t}const d=`${baseUrl}publicUpload?id=${e.id}&key=${e.apikey}`,n=document.createElement("tr");if(n.id=`row-${e.id}`,n.className="filerequest-item",n.appendChild(h(e.name,d)),e.maxfiles==0?n.appendChild(r(e.uploadedfiles)):n.appendChild(r(`${e.uploadedfiles} / ${e.maxfiles}`)),n.appendChild(r(getReadableSize(e.totalfilesize))),n.appendChild(r(formatTimestampWithNegative(e.lastupload,"None"))),n.appendChild(r(formatFileRequestExpiry(e.expiry))),canViewOtherRequests){let s=r(t);s.id=`cell-username-${e.id}`,n.appendChild(s)}const u=document.createElement("td"),l=document.createElement("div");l.className="btn-group",l.role="group";const o=document.createElement("button");o.id=`download-${e.id}`,o.type="button",o.className="btn btn-outline-light btn-sm",o.title="Download all",e.uploadedfiles==0&&o.classList.add("disabled"),o.appendChild(c("bi-download"));const s=document.createElement("button");s.id=`copy-${e.id}`,s.type="button",s.className="copyurl btn btn-outline-light btn-sm",s.title="Copy URL",s.setAttribute("data-clipboard-text",d),s.onclick=()=>showToast(1e3),s.appendChild(c("bi-copy"));const i=document.createElement("button");i.id=`edit-${e.id}`,i.type="button",i.className="btn btn-outline-light btn-sm",i.title="Edit request",i.onclick=()=>editFileRequest(e.id,e.name,e.maxfiles,e.maxsize,e.expiry,e.notes),i.appendChild(c("bi-pencil"));const a=document.createElement("button");return a.id=`delete-${e.id}`,a.type="button",a.className="btn btn-outline-danger btn-sm",a.title="Delete",a.onclick=()=>deleteOrShowModal(e.id,e.name,e.uploadedfiles),a.appendChild(c("bi-trash3")),l.append(o,s,i,a),u.appendChild(l),n.appendChild(u),n}function filterLogs(e){const t=document.getElementById("logviewer");e=="all"?t.value=logContent:t.value=logContent.split(` `).filter(t=>t.includes("["+e+"]")).join(` -`),t.scrollTop=t.scrollHeight}function setTrafficInfo(e,t){insertReadableSizeTwoOutputs(e,"totalTraffic","totalTrafficUnit"),document.getElementById("cardTraffic").title="Traffic since "+formatUnixTimestamp(t)}function setMemoryUsage(e,t){insertReadableSizeTwoOutputs(t,"totalMemory","memoryUnit");let n=document.getElementById("memoryUnit").innerText;insertReadableSizeForcedUnit(e,"usedMemory",n)}function setDiskUsage(e,t){insertReadableSizeTwoOutputs(t,"totalDisk","diskUnit");let n=document.getElementById("diskUnit").innerText;insertReadableSizeForcedUnit(e,"usedDisk",n)}function formatDuration(e){const t=[{label:"y",value:31536e3},{label:"d",value:86400},{label:"h",value:3600},{label:"m",value:60},{label:"s",value:1}];let n=t.findIndex(t=>e>=t.value);(n===-1||t[n].label==="s")&&(n=t.findIndex(e=>e.label==="m"));const s=t[n],o=t[n+1],i=Math.floor(e/s.value),a=e%s.value,r=Math.floor(a/o.value);return`${i}${s.label} ${r}${o.label}`}function addUptime(){if(currentUptime>3600)return;setTimeout(()=>{++currentUptime,document.getElementById("uptime").innerText=formatDuration(currentUptime),addUptime()},1e3)}function setPercentageBar(e,t,n){let o=t;n!==0[0]&&(o=t/n*100);const s=document.getElementById(e);s.classList.remove("bg-success"),s.classList.remove("bg-warning"),s.classList.remove("bg-danger"),o<70&&s.classList.add("bg-success"),o>=70&&o<90&&s.classList.add("bg-warning"),o>=90&&s.classList.add("bg-danger"),s.style.width=o+"%"}async function loadLogs(e){const t=document.getElementById("logviewer");try{const n=await apiLogGet(e);lastLogUpdate=n.timestamp;let s=!0;if(e!=0){if(n.logEntries=="")return;s=allowScroll(),logContent=logContent+n.logEntries}else logContent=n.logEntries;filterLogs(document.getElementById("logFilter").value),s&&(t.scrollTop=t.scrollHeight)}catch(e){lastLogUpdate=0,console.error("Failed to load logs:",e),t.value="Error loading logs. See console for details."}}async function loadStatus(){try{const e=await apiLogSystemStatus();currentUptime=e.uptime,document.getElementById("labelCpu").innerText=e.cpuLoad+"%",document.getElementById("labelActiveFiles").innerText=e.activeFiles,setPercentageBar("barCpu",e.cpuLoad),setPercentageBar("barDisk",e.diskUsagePercentage),setPercentageBar("barMemory",e.memoryUsagePercentage),setMemoryUsage(e.memoryUsed,e.memoryTotal),setDiskUsage(e.diskUsed,e.diskTotal),setTrafficInfo(e.dataServed,e.trafficRecordingSince)}catch(e){console.error("Failed to server status:",e)}}async function pollInfo(){for(firstStart=!0;!0;)await loadLogs(lastLogUpdate),firstStart?firstStart=!1:await loadStatus(),await new Promise(e=>setTimeout(e,POLL_INTERVAL_S*1e3))}function allowScroll(){const e=document.getElementById("logviewer");return e.scrollTop+e.clientHeight>=e.scrollHeight-5}function deleteLogs(){const n=document.getElementById("deleteLogsSel");if(!n)return;const t=n.value;if(t=="none"||t=="")return;if(!confirm("Do you want to delete the selected logs?")){document.getElementById("deleteLogs").selectedIndex=0;return}let e=Math.floor(Date.now()/1e3);switch(t){case"all":e=0;break;case"2":e=e-2*24*60*60;break;case"7":e=e-7*24*60*60;break;case"14":e=e-14*24*60*60;break;case"30":e=e-30*24*60*60;break;default:return}apiLogsDelete(e).then(e=>{location.reload()}).catch(e=>{alert("Unable to delete logs: "+e),console.error("Error:",e)})}function resetTrafficStat(){if(!confirm("Do you want to reset the traffic statistics?"))return;apiLogResetTraffic().then(e=>{location.reload()}).catch(e=>{alert("Unable to reset stats: "+e),console.error("Error:",e)})}isE2EEnabled=!1,isUploading=!1,rowCount=-1;function initDropzone(){Dropzone.options.uploaddropzone={paramName:"file",dictDefaultMessage:"",createImageThumbnails:!1,chunksUploaded:function(e,t){sendChunkComplete(e,t)},init:function(){dropzoneObject=this,this.on("addedfile",e=>{e.upload.uuid=getUuid(),saveUploadDefaults(),addFileProgress(e)}),this.on("queuecomplete",function(){isUploading=!1}),this.on("sending",function(){isUploading=!0}),this.on("error",function(e,t,n){if(console.log(t),n){if(n.status===413){showError(e,"File too large to upload. If you are using a reverse proxy, make sure that the allowed body size is at least 70MB.");return}try{console.log(n),errInfo=JSON.parse(n.responseText),showError(e,"Error: "+errInfo.ErrorMessage)}catch{showError(e,"Error: "+n.responseText)}}else showError(e,"Error: "+t)}),this.on("uploadprogress",function(e,t,n){updateProgressbar(e,t,n)}),isE2EEnabled&&(dropzoneObject.disable(),setE2eUpload())}},document.onpaste=function(e){if(dropzoneObject.disabled)return;const n=document.activeElement;if(n&&(n.hasAttribute("data-allow-regular-paste")||n.hasAttribute("placeholder")))return;var t,s=(e.clipboardData||e.originalEvent.clipboardData).items;for(let e in s)t=s[e],t.kind==="file"&&dropzoneObject.addFile(t.getAsFile()),t.kind==="string"&&t.getAsString(function(e){const t=//gi;if(t.test(e)===!1){let t=new Blob([e],{type:"text/plain"}),n=new File([t],"Pasted Text.txt",{type:"text/plain",lastModified:new Date(0)});dropzoneObject.addFile(n)}})},window.addEventListener("beforeunload",e=>{isUploading&&(e.returnValue="Upload is still in progress. Do you want to close this page?")})}function updateProgressbar(e,t,n){let o=e.upload.uuid,i=document.getElementById(`us-container-${o}`);if(i==null||i.getAttribute("data-complete")==="true")return;let s=Math.round(t);s<0&&(s=0),s>100&&(s=100);let r=Date.now()-i.getAttribute("data-starttime"),c=n/(r/1e3)/1024/1024;document.getElementById(`us-progressbar-${o}`).style.width=s+"%";let a=Math.round(c*10)/10;Number.isNaN(a)||(document.getElementById(`us-progress-info-${o}`).innerText=s+"% - "+a+"MB/s")}function addFileProgress(e){addFileStatus(e.upload.uuid,e.upload.filename)}function setUploadDefaults(){let s=getLocalStorageWithDefault("defaultDownloads",1),o=getLocalStorageWithDefault("defaultExpiry",14),e=getLocalStorageWithDefault("defaultPassword",""),t=getLocalStorageWithDefault("defaultUnlimitedDownloads",!1)==="true",n=getLocalStorageWithDefault("defaultUnlimitedTime",!1)==="true";document.getElementById("allowedDownloads").value=s,document.getElementById("expiryDays").value=o,document.getElementById("password").value=e,document.getElementById("enableDownloadLimit").checked=!t,document.getElementById("enableTimeLimit").checked=!n,e===""?(document.getElementById("enablePassword").checked=!1,document.getElementById("password").disabled=!0):(document.getElementById("enablePassword").checked=!0,document.getElementById("password").disabled=!1),t&&(document.getElementById("allowedDownloads").disabled=!0),n&&(document.getElementById("expiryDays").disabled=!0)}function saveUploadDefaults(){localStorage.setItem("defaultDownloads",document.getElementById("allowedDownloads").value),localStorage.setItem("defaultExpiry",document.getElementById("expiryDays").value),localStorage.setItem("defaultPassword",document.getElementById("password").value),localStorage.setItem("defaultUnlimitedDownloads",!document.getElementById("enableDownloadLimit").checked),localStorage.setItem("defaultUnlimitedTime",!document.getElementById("enableTimeLimit").checked)}function getLocalStorageWithDefault(e,t){var n=localStorage.getItem(e);return n===null?t:n}function urlencodeFormData(e){let t="";function s(e){return encodeURIComponent(e).replace(/%20/g,"+")}for(var n of e.entries())typeof n[1]=="string"&&(t+=(t?"&":"")+s(n[0])+"="+s(n[1]));return t}function sendChunkComplete(e,t){let c=e.upload.uuid,n=e.name,s=e.size,l=e.size,o=e.type,i=document.getElementById("allowedDownloads").value,a=document.getElementById("expiryDays").value,d=document.getElementById("password").value,r=e.isEndToEndEncrypted===!0,u=!0;document.getElementById("enableDownloadLimit").checked||(i=0),document.getElementById("enableTimeLimit").checked||(a=0),r&&(s=e.sizeEncrypted,n="Encrypted File",o=""),apiChunkComplete(c,n,s,l,o,i,a,d,r,u).then(n=>{t();let s=document.getElementById(`us-progress-info-${e.upload.uuid}`);s!=null&&(s.innerText="In Queue...")}).catch(t=>{console.error("Error:",t),dropzoneUploadError(e,t)})}function dropzoneUploadError(e,t){e.accepted=!1,dropzoneObject._errorProcessing([e],t),showError(e,t)}function dropzoneGetFile(e){for(let t=0;t{addRow(n),notifyWorker({type:"fileAdded",item:n});let s=dropzoneGetFile(t);if(s==null)return;s.isEndToEndEncrypted===!0?apiE2eMutexLockUnlock(!1).then(()=>apiE2eGet()).then(n=>{let i=GokapiE2EInfoParse(n);if(i instanceof Error)throw i;let a=GokapiE2EAddFile(t,e,s.name);if(a instanceof Error)throw a;let o=GokapiE2EInfoEncrypt();if(o instanceof Error)throw o;return apiE2eStore(o)}).then(()=>{GokapiE2EDecryptMenu(),removeFileStatus(t)}).catch(e=>{s.accepted=!1,dropzoneObject._errorProcessing([s],e),console.error("Error:",e)}).finally(()=>{apiE2eMutexLockUnlock(!0).catch(e=>{console.error("Failed to release E2E mutex after write: "+e)})}):removeFileStatus(t)}).catch(e=>{let n=dropzoneGetFile(t);n!=null&&dropzoneUploadError(n,e),console.error("Error:",e)})}function parseProgressStatus(e){let n=document.getElementById(`us-container-${e.chunk_id}`);if(n==null)return;n.setAttribute("data-complete","true");let t;switch(e.upload_status){case 0:t="Processing file...";break;case 1:t="Saving file...";break;case 2:t="Finalising...",requestFileInfo(e.file_id,e.chunk_id);break;case 3:t="Error";let n=dropzoneGetFile(e.chunk_id);e.error_message==""&&(e.error_message="Server Error"),n!=null&&dropzoneUploadError(n,e.error_message);return;default:t="Unknown status";break}document.getElementById(`us-progress-info-${e.chunk_id}`).innerText=t}function showError(e,t){let n=e.upload.uuid;document.getElementById(`us-progressbar-${n}`).style.width="100%",document.getElementById(`us-progressbar-${n}`).style.backgroundColor="red",document.getElementById(`us-progress-info-${n}`).innerText=t,document.getElementById(`us-progress-info-${n}`).classList.add("uploaderror")}function editFile(){const e=document.getElementById("mb_save");e.disabled=!0;let s=e.getAttribute("data-fileid"),o=document.getElementById("mi_edit_down").value,i=document.getElementById("mi_edit_expiry").value,t=document.getElementById("mi_edit_pw").value,a=t==="(unchanged)";document.getElementById("mc_download").checked||(o=0),document.getElementById("mc_expiry").checked||(i=0),document.getElementById("mc_password").checked||(a=!1,t="");let r=!1,n="";document.getElementById("mc_replace").checked&&(n=document.getElementById("mi_edit_replace").value,r=n!=""),apiFilesModify(s,o,i,t,a).then(t=>{if(!r){location.reload();return}apiFilesReplace(s,n).then(e=>{location.reload()}).catch(t=>{alert("Unable to edit file: "+t),console.error("Error:",t),e.disabled=!1})}).catch(t=>{alert("Unable to edit file: "+t),console.error("Error:",t),e.disabled=!1})}function showEditModal(e,t,n,s,o,i,a,r,c){let d=$("#modaledit").clone();$("#modaledit").on("hide.bs.modal",function(){$("#modaledit").remove();let e=d.clone();$("body").append(e)}),document.getElementById("m_filenamelabel").innerText=e,document.getElementById("mc_expiry").setAttribute("data-timestamp",s),document.getElementById("mb_save").setAttribute("data-fileid",t),createCalendar("mi_edit_expiry",s),i?(document.getElementById("mi_edit_down").value="1",document.getElementById("mi_edit_down").disabled=!0,document.getElementById("mc_download").checked=!1):(document.getElementById("mi_edit_down").value=n,document.getElementById("mi_edit_down").disabled=!1,document.getElementById("mc_download").checked=!0),a?(document.getElementById("mi_edit_expiry").value=add14DaysIfBeforeCurrentTime(s),document.getElementById("mi_edit_expiry").disabled=!0,document.getElementById("mc_expiry").checked=!1,calendarInstance._input.disabled=!0):(document.getElementById("mi_edit_expiry").value=s,document.getElementById("mi_edit_expiry").disabled=!1,document.getElementById("mc_expiry").checked=!0,calendarInstance._input.disabled=!1),o?(document.getElementById("mi_edit_pw").value="(unchanged)",document.getElementById("mi_edit_pw").disabled=!1,document.getElementById("mc_password").checked=!0):(document.getElementById("mi_edit_pw").value="",document.getElementById("mi_edit_pw").disabled=!0,document.getElementById("mc_password").checked=!1);let l=document.getElementById("mi_edit_replace");if(c)if(document.getElementById("replaceGroup").style.display="flex",r)document.getElementById("mc_replace").disabled=!0,document.getElementById("mc_replace").title="Replacing content is not available for end-to-end encrypted files",l.add(new Option("Unavailable",0)),l.title="Replacing content is not available for end-to-end encrypted files",l.value="0";else{let e=getAllAvailableFiles();for(let n=0;n{changeRowCount(!1,document.getElementById("row-"+e)),showToastFileDeletion(e),notifyWorker({type:"fileDeleted",id:e})}).catch(e=>{alert("Unable to delete file: "+e),console.error("Error:",e)})}function checkBoxChanged(e,t){let n=!e.checked;n?document.getElementById(t).setAttribute("disabled",""):document.getElementById(t).removeAttribute("disabled"),t==="password"&&n&&(document.getElementById("password").value="")}function parseSseData(e){let t;try{t=JSON.parse(e)}catch(e){console.error("Failed to parse event data:",e);return}switch(t.event){case"download":setNewDownloadCount(t.file_id,t.download_count,t.downloads_remaining);return;case"uploadStatus":parseProgressStatus(t);return;default:console.error("Unknown event",t)}}function setNewDownloadCount(e,t,n){let s=document.getElementById("cell-downloads-"+e);if(s!=null&&(s.innerText=t,s.classList.add("updatedDownloadCount"),setTimeout(()=>s.classList.remove("updatedDownloadCount"),500)),n!=-1){let t=document.getElementById("cell-downloadsRemaining-"+e);t!=null&&(t.innerText=n,t.classList.add("updatedDownloadCount"),setTimeout(()=>t.classList.remove("updatedDownloadCount"),500))}}sseWorkerPort=null;function notifyWorker(e){sseWorkerPort!==null&&sseWorkerPort.postMessage(e)}function registerChangeHandler(){if(typeof SharedWorker!="undefined")try{const e=new SharedWorker("./js/sse-worker.js");e.port.onmessage=e=>{if(e.data.type==="message")parseSseData(e.data.data);else if(e.data.type==="error")console.error("SSE worker connection error:",e.data.detail);else if(e.data.type==="shutdown")setTimeout(function(){window.location.href="./login"},1e3);else if(e.data.type==="fileAdded")document.getElementById("row-"+sanitizeId(e.data.item.Id))==null&&addRow(e.data.item);else if(e.data.type==="fileDeleted"){let t=document.getElementById("row-"+sanitizeId(e.data.id));t!=null&&changeRowCount(!1,t)}else if(e.data.type==="log"){const{level:t,message:n,detail:s}=e.data;s?console[t](n,s):console[t](n)}},e.onerror=e=>{console.warn("SharedWorker failed, falling back to direct SSE:",e),sseWorkerPort=null,_registerDirectSSE()},e.port.start(),sseWorkerPort=e.port;return}catch(e){console.warn("SharedWorker unavailable, falling back to direct SSE:",e)}_registerDirectSSE()}function _registerDirectSSE(){const e=new EventSource("./uploadStatus");e.onmessage=e=>{parseSseData(e.data)},e.onerror=t=>{t.target.readyState!==EventSource.CLOSED&&e.close(),console.log("Reconnecting to SSE (direct)..."),setTimeout(_registerDirectSSE,5e3)}}statusItemCount=0;function addFileStatus(e,t){const n=document.createElement("div");n.setAttribute("id",`us-container-${e}`),n.classList.add("us-container");const a=document.createElement("div");a.classList.add("filename"),a.textContent=t,n.appendChild(a);const s=document.createElement("div");s.classList.add("upload-progress-container"),s.setAttribute("id",`us-progress-container-${e}`);const r=document.createElement("div");r.classList.add("upload-progress-bar");const o=document.createElement("div");o.setAttribute("id",`us-progressbar-${e}`),o.classList.add("upload-progress-bar-progress"),o.style.width="0%",r.appendChild(o);const i=document.createElement("div");i.setAttribute("id",`us-progress-info-${e}`),i.classList.add("upload-progress-info"),i.textContent="0%",s.appendChild(r),s.appendChild(i),n.appendChild(s),n.setAttribute("data-starttime",Date.now()),n.setAttribute("data-complete","false");const c=document.getElementById("uploadstatus");c.appendChild(n),c.style.visibility="visible",statusItemCount++}function removeFileStatus(e){const t=document.getElementById(`us-container-${e}`);if(t==null)return;t.remove(),statusItemCount--,statusItemCount<1&&(document.getElementById("uploadstatus").style.visibility="hidden")}function addRow(e){let d=document.getElementById("downloadtable"),t=d.insertRow(0);e.Id=sanitizeId(e.Id),t.id="row-"+e.Id;let i=t.insertCell(0),a=t.insertCell(1),s=t.insertCell(2),r=t.insertCell(3),c=t.insertCell(4),o=t.insertCell(5),l=t.insertCell(6);i.innerText=e.Name,i.id="cell-name-"+e.Id,c.id="cell-downloads-"+e.Id,a.innerText=e.Size,e.UnlimitedDownloads?s.innerText="Unlimited":(s.innerText=e.DownloadsRemaining,s.id="cell-downloadsRemaining-"+e.Id),e.UnlimitedTime?r.innerText="Unlimited":r.innerText=formatUnixTimestamp(e.ExpireAt),c.innerText=e.DownloadCount;const n=document.createElement("a");if(n.href=e.UrlDownload,n.target="_blank",n.style.color="inherit",n.id="url-href-"+e.Id,n.textContent=e.Id,o.appendChild(n),e.IsPasswordProtected===!0){const e=document.createElement("i");e.className="bi bi-key",e.title="Password protected",o.appendChild(document.createTextNode(" ")),o.appendChild(e)}return l.appendChild(createButtonGroup(e)),i.classList.add("newItem"),a.classList.add("newItem"),s.classList.add("newItem"),r.classList.add("newItem"),c.classList.add("newItem"),o.classList.add("newItem"),l.classList.add("newItem"),a.setAttribute("data-order",e.SizeBytes),changeRowCount(!0,t),e.Id}function createButtonGroup(e){const m=document.createElement("div");m.className="btn-toolbar justify-content-end",m.setAttribute("role","toolbar");const t=document.createElement("div");t.className="btn-group me-2",t.setAttribute("role","group");const n=document.createElement("button");n.type="button",n.className="copyurl btn btn-outline-light btn-sm",n.dataset.clipboardText=e.UrlDownload,n.id="url-button-"+e.Id,n.title="Copy URL";const _=document.createElement("i");_.className="bi bi-copy",n.appendChild(_),n.appendChild(document.createTextNode(" URL")),n.addEventListener("click",()=>{showToast(1e3)}),t.appendChild(n);const f=document.createElement("button");f.type="button",f.className="btn btn-outline-light btn-sm dropdown-toggle dropdown-toggle-split",f.setAttribute("data-bs-toggle","dropdown"),f.setAttribute("aria-expanded","false"),t.appendChild(f);const p=document.createElement("ul");p.className="dropdown-menu dropdown-menu-end",p.setAttribute("data-bs-theme","dark");const b=document.createElement("li"),s=document.createElement("a");e.UrlHotlink!==""?(s.className="dropdown-item copyurl",s.title="Copy hotlink",s.style.cursor="pointer",s.setAttribute("data-clipboard-text",e.UrlHotlink),s.onclick=()=>showToast(1e3),s.innerHTML=` Hotlink`):(s.className="dropdown-item",s.innerText="Hotlink not available"),b.appendChild(s),p.appendChild(b),t.appendChild(p);const d=document.createElement("button");d.type="button",d.className="btn btn-outline-light btn-sm",d.title="Share",d.onclick=()=>shareUrl(event,e.Id),d.innerHTML=` +`),t.scrollTop=t.scrollHeight}function setTrafficInfo(e,t){insertReadableSizeTwoOutputs(e,"totalTraffic","totalTrafficUnit"),document.getElementById("cardTraffic").title="Traffic since "+formatUnixTimestamp(t)}function setMemoryUsage(e,t){insertReadableSizeTwoOutputs(t,"totalMemory","memoryUnit");let n=document.getElementById("memoryUnit").innerText;insertReadableSizeForcedUnit(e,"usedMemory",n)}function setDiskUsage(e,t){insertReadableSizeTwoOutputs(t,"totalDisk","diskUnit");let n=document.getElementById("diskUnit").innerText;insertReadableSizeForcedUnit(e,"usedDisk",n)}function formatDuration(e){const t=[{label:"y",value:31536e3},{label:"d",value:86400},{label:"h",value:3600},{label:"m",value:60},{label:"s",value:1}];let n=t.findIndex(t=>e>=t.value);(n===-1||t[n].label==="s")&&(n=t.findIndex(e=>e.label==="m"));const s=t[n],o=t[n+1],i=Math.floor(e/s.value),a=e%s.value,r=Math.floor(a/o.value);return`${i}${s.label} ${r}${o.label}`}function addUptime(){if(currentUptime>3600)return;setTimeout(()=>{++currentUptime,document.getElementById("uptime").innerText=formatDuration(currentUptime),addUptime()},1e3)}function setPercentageBar(e,t,n){let o=t;n!==0[0]&&(o=t/n*100);const s=document.getElementById(e);s.classList.remove("bg-success"),s.classList.remove("bg-warning"),s.classList.remove("bg-danger"),o<70&&s.classList.add("bg-success"),o>=70&&o<90&&s.classList.add("bg-warning"),o>=90&&s.classList.add("bg-danger"),s.style.width=o+"%"}async function loadLogs(e){const t=document.getElementById("logviewer");try{const n=await apiLogGet(e);lastLogUpdate=n.timestamp;let s=!0;if(e!=0){if(n.logEntries=="")return;s=allowScroll(),logContent=logContent+n.logEntries}else logContent=n.logEntries;filterLogs(document.getElementById("logFilter").value),s&&(t.scrollTop=t.scrollHeight)}catch(e){lastLogUpdate=0,console.error("Failed to load logs:",e),t.value="Error loading logs. See console for details."}}async function loadStatus(){try{const e=await apiLogSystemStatus();currentUptime=e.uptime,document.getElementById("labelCpu").innerText=e.cpuLoad+"%",document.getElementById("labelActiveFiles").innerText=e.activeFiles,setPercentageBar("barCpu",e.cpuLoad),setPercentageBar("barDisk",e.diskUsagePercentage),setPercentageBar("barMemory",e.memoryUsagePercentage),setMemoryUsage(e.memoryUsed,e.memoryTotal),setDiskUsage(e.diskUsed,e.diskTotal),setTrafficInfo(e.dataServed,e.trafficRecordingSince)}catch(e){console.error("Failed to server status:",e)}}async function pollInfo(){for(firstStart=!0;!0;)await loadLogs(lastLogUpdate),firstStart?firstStart=!1:await loadStatus(),await new Promise(e=>setTimeout(e,POLL_INTERVAL_S*1e3))}function allowScroll(){const e=document.getElementById("logviewer");return e.scrollTop+e.clientHeight>=e.scrollHeight-5}function deleteLogs(){const n=document.getElementById("deleteLogsSel");if(!n)return;const t=n.value;if(t=="none"||t=="")return;if(!confirm("Do you want to delete the selected logs?")){document.getElementById("deleteLogs").selectedIndex=0;return}let e=Math.floor(Date.now()/1e3);switch(t){case"all":e=0;break;case"2":e=e-2*24*60*60;break;case"7":e=e-7*24*60*60;break;case"14":e=e-14*24*60*60;break;case"30":e=e-30*24*60*60;break;default:return}apiLogsDelete(e).then(e=>{location.reload()}).catch(e=>{alert("Unable to delete logs: "+e),console.error("Error:",e)})}function resetTrafficStat(){if(!confirm("Do you want to reset the traffic statistics?"))return;apiLogResetTraffic().then(e=>{location.reload()}).catch(e=>{alert("Unable to reset stats: "+e),console.error("Error:",e)})}function pasteToggleDownloads(e){document.getElementById("paste-downloads").disabled=!e.checked}function pasteToggleExpiry(e){document.getElementById("paste-expiry").disabled=!e.checked}function pasteTogglePassword(e){document.getElementById("paste-password").disabled=!e.checked}function submitPaste(){const e=document.getElementById("paste-content").value.trim();if(!e){alert("Please enter some content before creating a paste.");return}const t=document.getElementById("paste-title").value.trim(),n=document.getElementById("paste-enable-downloads").checked,s=document.getElementById("paste-enable-expiry").checked,o=document.getElementById("paste-enable-password").checked,i=n?parseInt(document.getElementById("paste-downloads").value,10):0,a=s?parseInt(document.getElementById("paste-expiry").value,10):0,r=o?document.getElementById("paste-password").value:"";apiAddPaste(e,t,i,a,r).then(e=>{pasteInsertRow(e),document.getElementById("paste-content").value="",document.getElementById("paste-title").value="",pasteCopyUrl(e.UrlDownload,e.Id)}).catch(e=>{alert("Failed to create paste: "+e),console.error("Error:",e)})}function pasteInsertRow(e){const l=document.getElementById("paste-tbody"),o=document.createElement("tr");o.id="pasterow-"+e.Id;const d=e.UnlimitedDownloads?"Unlimited":e.DownloadsRemaining,a=[{text:e.Name},{id:`paste-created-${e.Id}`},{text:String(e.DownloadCount)},{id:`paste-expiry-${e.Id}`},{text:String(d)}];canViewOtherUploads&&a.push({text:userNameSelf});for(const t of a){const e=document.createElement("td");if(e.className="newItem",t.id){const n=document.createElement("span");n.id=t.id,e.appendChild(n)}else e.textContent=t.text;o.appendChild(e)}const i=document.createElement("td");i.className="newItem";const s=document.createElement("div");s.className="btn-group",s.role="group";const t=document.createElement("button");t.type="button",t.className="btn btn-outline-light btn-sm",t.title="Copy URL",t.addEventListener("click",()=>pasteCopyUrl(e.UrlDownload,e.Id));const r=document.createElement("i");r.className="bi bi-copy",t.appendChild(r);const n=document.createElement("button");n.type="button",n.className="btn btn-outline-danger btn-sm",n.title="Delete",n.addEventListener("click",()=>pasteDelete(e.Id));const c=document.createElement("i");c.className="bi bi-trash3",n.appendChild(c),s.appendChild(t),s.appendChild(n),i.appendChild(s),o.appendChild(i),l.prepend(o),insertDateWithNegative(e.UploadDate,`paste-created-${e.Id}`,"Unknown"),e.UnlimitedTime?document.getElementById(`paste-expiry-${e.Id}`).innerText="Never":insertFileRequestExpiry(e.ExpireAt,`paste-expiry-${e.Id}`)}function pasteCopyUrl(e){navigator.clipboard.writeText(e).then(()=>{showToast(1e3)}).catch(()=>{})}function pasteDelete(e){if(!confirm("Delete this paste?"))return;apiFilesDelete(e,0).then(()=>{const t=document.getElementById("pasterow-"+e);t&&(t.classList.add("rowDeleting"),setTimeout(()=>t.remove(),290))}).catch(e=>{alert("Failed to delete paste: "+e),console.error("Error:",e)})}function escapeHtml(e){return String(e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}isE2EEnabled=!1,isUploading=!1,rowCount=-1;function initDropzone(){Dropzone.options.uploaddropzone={paramName:"file",dictDefaultMessage:"",createImageThumbnails:!1,chunksUploaded:function(e,t){sendChunkComplete(e,t)},init:function(){dropzoneObject=this,this.on("addedfile",e=>{e.upload.uuid=getUuid(),saveUploadDefaults(),addFileProgress(e)}),this.on("queuecomplete",function(){isUploading=!1}),this.on("sending",function(){isUploading=!0}),this.on("error",function(e,t,n){if(console.log(t),n){if(n.status===413){showError(e,"File too large to upload. If you are using a reverse proxy, make sure that the allowed body size is at least 70MB.");return}try{console.log(n),errInfo=JSON.parse(n.responseText),showError(e,"Error: "+errInfo.ErrorMessage)}catch{showError(e,"Error: "+n.responseText)}}else showError(e,"Error: "+t)}),this.on("uploadprogress",function(e,t,n){updateProgressbar(e,t,n)}),isE2EEnabled&&(dropzoneObject.disable(),setE2eUpload())}},document.onpaste=function(e){if(dropzoneObject.disabled)return;const n=document.activeElement;if(n&&(n.hasAttribute("data-allow-regular-paste")||n.hasAttribute("placeholder")))return;var t,s=(e.clipboardData||e.originalEvent.clipboardData).items;for(let e in s)t=s[e],t.kind==="file"&&dropzoneObject.addFile(t.getAsFile()),t.kind==="string"&&t.getAsString(function(e){const t=//gi;if(t.test(e)===!1){let t=new Blob([e],{type:"text/plain"}),n=new File([t],"Pasted Text.txt",{type:"text/plain",lastModified:new Date(0)});dropzoneObject.addFile(n)}})},window.addEventListener("beforeunload",e=>{isUploading&&(e.returnValue="Upload is still in progress. Do you want to close this page?")})}function updateProgressbar(e,t,n){let o=e.upload.uuid,i=document.getElementById(`us-container-${o}`);if(i==null||i.getAttribute("data-complete")==="true")return;let s=Math.round(t);s<0&&(s=0),s>100&&(s=100);let r=Date.now()-i.getAttribute("data-starttime"),c=n/(r/1e3)/1024/1024;document.getElementById(`us-progressbar-${o}`).style.width=s+"%";let a=Math.round(c*10)/10;Number.isNaN(a)||(document.getElementById(`us-progress-info-${o}`).innerText=s+"% - "+a+"MB/s")}function addFileProgress(e){addFileStatus(e.upload.uuid,e.upload.filename)}function setUploadDefaults(){let s=getLocalStorageWithDefault("defaultDownloads",1),o=getLocalStorageWithDefault("defaultExpiry",14),e=getLocalStorageWithDefault("defaultPassword",""),t=getLocalStorageWithDefault("defaultUnlimitedDownloads",!1)==="true",n=getLocalStorageWithDefault("defaultUnlimitedTime",!1)==="true";document.getElementById("allowedDownloads").value=s,document.getElementById("expiryDays").value=o,document.getElementById("password").value=e,document.getElementById("enableDownloadLimit").checked=!t,document.getElementById("enableTimeLimit").checked=!n,e===""?(document.getElementById("enablePassword").checked=!1,document.getElementById("password").disabled=!0):(document.getElementById("enablePassword").checked=!0,document.getElementById("password").disabled=!1),t&&(document.getElementById("allowedDownloads").disabled=!0),n&&(document.getElementById("expiryDays").disabled=!0)}function saveUploadDefaults(){localStorage.setItem("defaultDownloads",document.getElementById("allowedDownloads").value),localStorage.setItem("defaultExpiry",document.getElementById("expiryDays").value),localStorage.setItem("defaultPassword",document.getElementById("password").value),localStorage.setItem("defaultUnlimitedDownloads",!document.getElementById("enableDownloadLimit").checked),localStorage.setItem("defaultUnlimitedTime",!document.getElementById("enableTimeLimit").checked)}function getLocalStorageWithDefault(e,t){var n=localStorage.getItem(e);return n===null?t:n}function urlencodeFormData(e){let t="";function s(e){return encodeURIComponent(e).replace(/%20/g,"+")}for(var n of e.entries())typeof n[1]=="string"&&(t+=(t?"&":"")+s(n[0])+"="+s(n[1]));return t}function sendChunkComplete(e,t){let c=e.upload.uuid,n=e.name,s=e.size,l=e.size,o=e.type,i=document.getElementById("allowedDownloads").value,a=document.getElementById("expiryDays").value,d=document.getElementById("password").value,r=e.isEndToEndEncrypted===!0,u=!0;document.getElementById("enableDownloadLimit").checked||(i=0),document.getElementById("enableTimeLimit").checked||(a=0),r&&(s=e.sizeEncrypted,n="Encrypted File",o=""),apiChunkComplete(c,n,s,l,o,i,a,d,r,u).then(n=>{t();let s=document.getElementById(`us-progress-info-${e.upload.uuid}`);s!=null&&(s.innerText="In Queue...")}).catch(t=>{console.error("Error:",t),dropzoneUploadError(e,t)})}function dropzoneUploadError(e,t){e.accepted=!1,dropzoneObject._errorProcessing([e],t),showError(e,t)}function dropzoneGetFile(e){for(let t=0;t{addRow(n),notifyWorker({type:"fileAdded",item:n});let s=dropzoneGetFile(t);if(s==null)return;s.isEndToEndEncrypted===!0?apiE2eMutexLockUnlock(!1).then(()=>apiE2eGet()).then(n=>{let i=GokapiE2EInfoParse(n);if(i instanceof Error)throw i;let a=GokapiE2EAddFile(t,e,s.name);if(a instanceof Error)throw a;let o=GokapiE2EInfoEncrypt();if(o instanceof Error)throw o;return apiE2eStore(o)}).then(()=>{GokapiE2EDecryptMenu(),removeFileStatus(t)}).catch(e=>{s.accepted=!1,dropzoneObject._errorProcessing([s],e),console.error("Error:",e)}).finally(()=>{apiE2eMutexLockUnlock(!0).catch(e=>{console.error("Failed to release E2E mutex after write: "+e)})}):removeFileStatus(t)}).catch(e=>{let n=dropzoneGetFile(t);n!=null&&dropzoneUploadError(n,e),console.error("Error:",e)})}function parseProgressStatus(e){let n=document.getElementById(`us-container-${e.chunk_id}`);if(n==null)return;n.setAttribute("data-complete","true");let t;switch(e.upload_status){case 0:t="Processing file...";break;case 1:t="Saving file...";break;case 2:t="Finalising...",requestFileInfo(e.file_id,e.chunk_id);break;case 3:t="Error";let n=dropzoneGetFile(e.chunk_id);e.error_message==""&&(e.error_message="Server Error"),n!=null&&dropzoneUploadError(n,e.error_message);return;default:t="Unknown status";break}document.getElementById(`us-progress-info-${e.chunk_id}`).innerText=t}function showError(e,t){let n=e.upload.uuid;document.getElementById(`us-progressbar-${n}`).style.width="100%",document.getElementById(`us-progressbar-${n}`).style.backgroundColor="red",document.getElementById(`us-progress-info-${n}`).innerText=t,document.getElementById(`us-progress-info-${n}`).classList.add("uploaderror")}function editFile(){const e=document.getElementById("mb_save");e.disabled=!0;let s=e.getAttribute("data-fileid"),o=document.getElementById("mi_edit_down").value,i=document.getElementById("mi_edit_expiry").value,t=document.getElementById("mi_edit_pw").value,a=t==="(unchanged)";document.getElementById("mc_download").checked||(o=0),document.getElementById("mc_expiry").checked||(i=0),document.getElementById("mc_password").checked||(a=!1,t="");let r=!1,n="";document.getElementById("mc_replace").checked&&(n=document.getElementById("mi_edit_replace").value,r=n!=""),apiFilesModify(s,o,i,t,a).then(t=>{if(!r){location.reload();return}apiFilesReplace(s,n).then(e=>{location.reload()}).catch(t=>{alert("Unable to edit file: "+t),console.error("Error:",t),e.disabled=!1})}).catch(t=>{alert("Unable to edit file: "+t),console.error("Error:",t),e.disabled=!1})}function showEditModal(e,t,n,s,o,i,a,r,c){let d=$("#modaledit").clone();$("#modaledit").on("hide.bs.modal",function(){$("#modaledit").remove();let e=d.clone();$("body").append(e)}),document.getElementById("m_filenamelabel").innerText=e,document.getElementById("mc_expiry").setAttribute("data-timestamp",s),document.getElementById("mb_save").setAttribute("data-fileid",t),createCalendar("mi_edit_expiry",s),i?(document.getElementById("mi_edit_down").value="1",document.getElementById("mi_edit_down").disabled=!0,document.getElementById("mc_download").checked=!1):(document.getElementById("mi_edit_down").value=n,document.getElementById("mi_edit_down").disabled=!1,document.getElementById("mc_download").checked=!0),a?(document.getElementById("mi_edit_expiry").value=add14DaysIfBeforeCurrentTime(s),document.getElementById("mi_edit_expiry").disabled=!0,document.getElementById("mc_expiry").checked=!1,calendarInstance._input.disabled=!0):(document.getElementById("mi_edit_expiry").value=s,document.getElementById("mi_edit_expiry").disabled=!1,document.getElementById("mc_expiry").checked=!0,calendarInstance._input.disabled=!1),o?(document.getElementById("mi_edit_pw").value="(unchanged)",document.getElementById("mi_edit_pw").disabled=!1,document.getElementById("mc_password").checked=!0):(document.getElementById("mi_edit_pw").value="",document.getElementById("mi_edit_pw").disabled=!0,document.getElementById("mc_password").checked=!1);let l=document.getElementById("mi_edit_replace");if(c)if(document.getElementById("replaceGroup").style.display="flex",r)document.getElementById("mc_replace").disabled=!0,document.getElementById("mc_replace").title="Replacing content is not available for end-to-end encrypted files",l.add(new Option("Unavailable",0)),l.title="Replacing content is not available for end-to-end encrypted files",l.value="0";else{let e=getAllAvailableFiles();for(let n=0;n{changeRowCount(!1,document.getElementById("row-"+e)),showToastFileDeletion(e),notifyWorker({type:"fileDeleted",id:e})}).catch(e=>{alert("Unable to delete file: "+e),console.error("Error:",e)})}function checkBoxChanged(e,t){let n=!e.checked;n?document.getElementById(t).setAttribute("disabled",""):document.getElementById(t).removeAttribute("disabled"),t==="password"&&n&&(document.getElementById("password").value="")}function parseSseData(e){let t;try{t=JSON.parse(e)}catch(e){console.error("Failed to parse event data:",e);return}switch(t.event){case"download":setNewDownloadCount(t.file_id,t.download_count,t.downloads_remaining);return;case"uploadStatus":parseProgressStatus(t);return;default:console.error("Unknown event",t)}}function setNewDownloadCount(e,t,n){let s=document.getElementById("cell-downloads-"+e);if(s!=null&&(s.innerText=t,s.classList.add("updatedDownloadCount"),setTimeout(()=>s.classList.remove("updatedDownloadCount"),500)),n!=-1){let t=document.getElementById("cell-downloadsRemaining-"+e);t!=null&&(t.innerText=n,t.classList.add("updatedDownloadCount"),setTimeout(()=>t.classList.remove("updatedDownloadCount"),500))}}sseWorkerPort=null;function notifyWorker(e){sseWorkerPort!==null&&sseWorkerPort.postMessage(e)}function registerChangeHandler(){if(typeof SharedWorker!="undefined")try{const e=new SharedWorker("./js/sse-worker.js");e.port.onmessage=e=>{if(e.data.type==="message")parseSseData(e.data.data);else if(e.data.type==="error")console.error("SSE worker connection error:",e.data.detail);else if(e.data.type==="shutdown")setTimeout(function(){window.location.href="./login"},1e3);else if(e.data.type==="fileAdded")document.getElementById("row-"+sanitizeId(e.data.item.Id))==null&&addRow(e.data.item);else if(e.data.type==="fileDeleted"){let t=document.getElementById("row-"+sanitizeId(e.data.id));t!=null&&changeRowCount(!1,t)}else if(e.data.type==="log"){const{level:t,message:n,detail:s}=e.data;s?console[t](n,s):console[t](n)}},e.onerror=e=>{console.warn("SharedWorker failed, falling back to direct SSE:",e),sseWorkerPort=null,_registerDirectSSE()},e.port.start(),sseWorkerPort=e.port;return}catch(e){console.warn("SharedWorker unavailable, falling back to direct SSE:",e)}_registerDirectSSE()}function _registerDirectSSE(){const e=new EventSource("./uploadStatus");e.onmessage=e=>{parseSseData(e.data)},e.onerror=t=>{t.target.readyState!==EventSource.CLOSED&&e.close(),console.log("Reconnecting to SSE (direct)..."),setTimeout(_registerDirectSSE,5e3)}}statusItemCount=0;function addFileStatus(e,t){const n=document.createElement("div");n.setAttribute("id",`us-container-${e}`),n.classList.add("us-container");const a=document.createElement("div");a.classList.add("filename"),a.textContent=t,n.appendChild(a);const s=document.createElement("div");s.classList.add("upload-progress-container"),s.setAttribute("id",`us-progress-container-${e}`);const r=document.createElement("div");r.classList.add("upload-progress-bar");const o=document.createElement("div");o.setAttribute("id",`us-progressbar-${e}`),o.classList.add("upload-progress-bar-progress"),o.style.width="0%",r.appendChild(o);const i=document.createElement("div");i.setAttribute("id",`us-progress-info-${e}`),i.classList.add("upload-progress-info"),i.textContent="0%",s.appendChild(r),s.appendChild(i),n.appendChild(s),n.setAttribute("data-starttime",Date.now()),n.setAttribute("data-complete","false");const c=document.getElementById("uploadstatus");c.appendChild(n),c.style.visibility="visible",statusItemCount++}function removeFileStatus(e){const t=document.getElementById(`us-container-${e}`);if(t==null)return;t.remove(),statusItemCount--,statusItemCount<1&&(document.getElementById("uploadstatus").style.visibility="hidden")}function addRow(e){let d=document.getElementById("downloadtable"),t=d.insertRow(0);e.Id=sanitizeId(e.Id),t.id="row-"+e.Id;let i=t.insertCell(0),a=t.insertCell(1),s=t.insertCell(2),r=t.insertCell(3),c=t.insertCell(4),o=t.insertCell(5),l=t.insertCell(6);i.innerText=e.Name,i.id="cell-name-"+e.Id,c.id="cell-downloads-"+e.Id,a.innerText=e.Size,e.UnlimitedDownloads?s.innerText="Unlimited":(s.innerText=e.DownloadsRemaining,s.id="cell-downloadsRemaining-"+e.Id),e.UnlimitedTime?r.innerText="Unlimited":r.innerText=formatUnixTimestamp(e.ExpireAt),c.innerText=e.DownloadCount;const n=document.createElement("a");if(n.href=e.UrlDownload,n.target="_blank",n.style.color="inherit",n.id="url-href-"+e.Id,n.textContent=e.Id,o.appendChild(n),e.IsPasswordProtected===!0){const e=document.createElement("i");e.className="bi bi-key",e.title="Password protected",o.appendChild(document.createTextNode(" ")),o.appendChild(e)}return l.appendChild(createButtonGroup(e)),i.classList.add("newItem"),a.classList.add("newItem"),s.classList.add("newItem"),r.classList.add("newItem"),c.classList.add("newItem"),o.classList.add("newItem"),l.classList.add("newItem"),a.setAttribute("data-order",e.SizeBytes),changeRowCount(!0,t),e.Id}function createButtonGroup(e){const m=document.createElement("div");m.className="btn-toolbar justify-content-end",m.setAttribute("role","toolbar");const t=document.createElement("div");t.className="btn-group me-2",t.setAttribute("role","group");const n=document.createElement("button");n.type="button",n.className="copyurl btn btn-outline-light btn-sm",n.dataset.clipboardText=e.UrlDownload,n.id="url-button-"+e.Id,n.title="Copy URL";const _=document.createElement("i");_.className="bi bi-copy",n.appendChild(_),n.appendChild(document.createTextNode(" URL")),n.addEventListener("click",()=>{showToast(1e3)}),t.appendChild(n);const f=document.createElement("button");f.type="button",f.className="btn btn-outline-light btn-sm dropdown-toggle dropdown-toggle-split",f.setAttribute("data-bs-toggle","dropdown"),f.setAttribute("aria-expanded","false"),t.appendChild(f);const p=document.createElement("ul");p.className="dropdown-menu dropdown-menu-end",p.setAttribute("data-bs-theme","dark");const b=document.createElement("li"),s=document.createElement("a");e.UrlHotlink!==""?(s.className="dropdown-item copyurl",s.title="Copy hotlink",s.style.cursor="pointer",s.setAttribute("data-clipboard-text",e.UrlHotlink),s.onclick=()=>showToast(1e3),s.innerHTML=` Hotlink`):(s.className="dropdown-item",s.innerText="Hotlink not available"),b.appendChild(s),p.appendChild(b),t.appendChild(p);const d=document.createElement("button");d.type="button",d.className="btn btn-outline-light btn-sm",d.title="Share",d.onclick=()=>shareUrl(event,e.Id),d.innerHTML=` `,t.appendChild(d);const u=document.createElement("button");u.type="button",u.className="btn btn-outline-light btn-sm dropdown-toggle dropdown-toggle-split",u.setAttribute("data-bs-toggle","dropdown"),u.setAttribute("aria-expanded","false"),u.id=`shareDropdown-${e.Id}`,t.appendChild(u);const h=document.createElement("ul");h.className="dropdown-menu dropdown-menu-end",h.setAttribute("data-bs-theme","dark");const g=document.createElement("li"),o=document.createElement("a");o.className="dropdown-item",o.id=`qrcode-${e.Id}`,o.style.cursor="pointer",o.title="Open QR Code",o.onclick=()=>showQrCode(e.UrlDownload),o.innerHTML=` QR Code`,g.appendChild(o),h.appendChild(g);const v=document.createElement("li"),r=document.createElement("a");r.className="dropdown-item",r.title="Share via email",r.id=`email-${e.Id}`,r.target="_blank",r.href=`mailto:?body=${encodeURIComponent(e.UrlDownload)}`,r.innerHTML=` Email`,v.appendChild(r),h.appendChild(v),t.appendChild(h);const l=document.createElement("div");l.className="btn-group",l.setAttribute("role","group");const a=document.createElement("button");a.type="button",a.className="btn btn-outline-light btn-sm",a.title="Download",e.RequiresClientSideDecryption&&a.classList.add("disabled");const j=document.createElement("i");j.className="bi bi-download",a.appendChild(j),a.addEventListener("click",()=>{downloadFileWithPresign(e.Id)}),l.appendChild(a);const c=document.createElement("button");c.type="button",c.className="btn btn-outline-light btn-sm",c.title="Edit";const y=document.createElement("i");y.className="bi bi-pencil",c.appendChild(y),c.addEventListener("click",()=>{showEditModal(e.Name,e.Id,e.DownloadsRemaining,e.ExpireAt,e.IsPasswordProtected,e.UnlimitedDownloads,e.UnlimitedTime,e.IsEndToEndEncrypted,canReplaceOwnFiles)}),l.appendChild(c);const i=document.createElement("button");i.type="button",i.className="btn btn-outline-danger btn-sm",i.title="Delete",i.id="button-delete-"+e.Id;const w=document.createElement("i");return w.className="bi bi-trash3",i.appendChild(w),i.addEventListener("click",()=>{deleteFile(e.Id)}),l.appendChild(i),m.appendChild(t),m.appendChild(l),m}function sanitizeId(e){return e.replace(/[^a-zA-Z0-9]/g,"")}function changeRowCount(e,t){let n=$("#maintable").DataTable();rowCount==-1&&(rowCount=n.rows().count()),e?(++rowCount,n.row.add(t)):(--rowCount,t.classList.add("rowDeleting"),setTimeout(()=>{n.row(t).remove(),t.remove()},290));let s=document.getElementsByClassName("dataTables_empty")[0];typeof s!="undefined"?s.innerText="Files stored: "+rowCount:document.getElementsByClassName("dataTables_info")[0].innerText="Files stored: "+rowCount}function hideQrCode(){document.getElementById("qroverlay").style.display="none",document.getElementById("qrcode").innerHTML=""}function showQrCode(e){const t=document.getElementById("qroverlay");t.style.display="block",new QRCode(document.getElementById("qrcode"),{text:e,width:200,height:200,colorDark:"#000000",colorLight:"#ffffff",correctLevel:QRCode.CorrectLevel.H}),t.addEventListener("click",hideQrCode)}function showToastFileDeletion(e){let t=document.getElementById("toastnotificationUndo"),n=document.getElementById("cell-name-"+e).innerText,s=document.getElementById("toastFilename"),o=document.getElementById("toastUndoButton");s.innerText=n,o.dataset.fileid=e,hideToast(),t.classList.add("show"),clearTimeout(toastId),toastId=setTimeout(()=>{hideFileToast()},5e3)}function hideFileToast(){document.getElementById("toastnotificationUndo").classList.remove("show")}function handleUndo(e){hideFileToast(),apiFilesRestore(e.dataset.fileid).then(e=>{addRow(e.FileInfo),notifyWorker({type:"fileAdded",item:e.FileInfo}),isE2EEnabled&&GokapiE2EDecryptMenu()}).catch(e=>{alert("Unable to restore file: "+e),console.error("Error:",e)})}function shareUrl(e,t){if(!navigator.share){e.stopPropagation(),bootstrap.Dropdown.getOrCreateInstance(document.getElementById(`shareDropdown-${t}`)).toggle();return}let n=document.getElementById("cell-name-"+t).innerText,s=document.getElementById("url-href-"+t).getAttribute("href");navigator.share({title:n,url:s})}function showDeprecationNotice(){let e=document.getElementById("toastDeprecation");e.classList.add("show"),setTimeout(()=>{e.classList.remove("show")},5e3)}function changeUserPermission(e,t,n){let s=document.getElementById(n);if(s.classList.contains("perm-processing")||s.classList.contains("perm-nochange"))return;let o=s.classList.contains("perm-granted");s.classList.add("perm-processing"),s.classList.remove("perm-granted"),s.classList.remove("perm-notgranted");let i="GRANT";o&&(i="REVOKE"),t=="PERM_REPLACE_OTHER"&&!o&&(hasNotPermissionReplace=document.getElementById("perm_replace_"+e).classList.contains("perm-notgranted"),hasNotPermissionReplace&&(showToast(2e3,"Also granting permission to replace own files"),changeUserPermission(e,"PERM_REPLACE","perm_replace_"+e))),t=="PERM_REPLACE"&&o&&(hasPermissionReplaceOthers=document.getElementById("perm_replace_other_"+e).classList.contains("perm-granted"),hasPermissionReplaceOthers&&(showToast(2e3,"Also revoking permission to replace files of other users"),changeUserPermission(e,"PERM_REPLACE_OTHER","perm_replace_other_"+e))),apiUserModify(e,t,i).then(e=>{o?s.classList.add("perm-notgranted"):s.classList.add("perm-granted"),s.classList.remove("perm-processing")}).catch(e=>{o?s.classList.add("perm-granted"):s.classList.add("perm-notgranted"),s.classList.remove("perm-processing"),alert("Unable to set permission: "+e),console.error("Error:",e)})}function changeRank(e,t,n){let s=document.getElementById(n);if(s.disabled)return;s.disabled=!0,apiUserChangeRank(e,t).then(e=>{location.reload()}).catch(e=>{s.disabled=!1,alert("Unable to change rank: "+e),console.error("Error:",e)})}function showDeleteUserModal(e,t){let n=document.getElementById("checkboxDelete");n.checked=!1,document.getElementById("deleteModalBody").innerText=t,$("#deleteModal").modal("show"),document.getElementById("buttonDelete").onclick=function(){apiUserDelete(e,n.checked).then(t=>{$("#deleteModal").modal("hide"),document.getElementById("row-"+e).classList.add("rowDeleting"),setTimeout(()=>{document.getElementById("row-"+e).remove()},290)}).catch(e=>{alert("Unable to delete user: "+e),console.error("Error:",e)})}}function showAddUserModal(){let e=$("#newUserModal").clone();$("#newUserModal").on("hide.bs.modal",function(){$("#newUserModal").remove();let t=e.clone();$("body").append(t)}),$("#newUserModal").modal("show")}function showResetPwModal(e,t){let n=$("#resetPasswordModal").clone();$("#resetPasswordModal").on("hide.bs.modal",function(){$("#resetPasswordModal").remove();let e=n.clone();$("body").append(e)}),document.getElementById("l_userpwreset").innerText=t;let s=document.getElementById("resetPasswordButton");s.onclick=function(){resetPw(e,document.getElementById("generateRandomPassword").checked)},$("#resetPasswordModal").modal("show")}function resetPw(e,t){let n=document.getElementById("resetPasswordButton");document.getElementById("resetPasswordButton").disabled=!0,apiUserResetPassword(e,t).then(e=>{if(!t){$("#resetPasswordModal").modal("hide"),showToast(1e3,"Password change requirement set successfully");return}n.style.display="none",document.getElementById("cancelPasswordButton").style.display="none",document.getElementById("formentryReset").style.display="none",document.getElementById("randomPasswordContainer").style.display="block",document.getElementById("closeModalResetPw").style.display="block",document.getElementById("l_returnedPw").innerText=e.password,document.getElementById("copypwclip").onclick=function(){navigator.clipboard.writeText(e.password),showToast(1e3,"Password copied to clipboard")}}).catch(e=>{alert("Unable to reset user password: "+e),console.error("Error:",e),n.disabled=!1})}function addNewUser(){let e=document.getElementById("mb_addUser");e.disabled=!0;let t=document.getElementById("newUserForm");if(t.checkValidity()){let t=document.getElementById("e_userName");apiUserCreate(t.value.trim()).then(e=>{$("#newUserModal").modal("hide"),addRowUser(e.id,e.name,e.permissions),console.log(e)}).catch(t=>{t.message=="duplicate"?(alert("A user already exists with that name"),e.disabled=!1):(alert("Unable to create user: "+t),console.error("Error:",t),e.disabled=!1)})}else t.classList.add("was-validated"),e.disabled=!1}const PermissionDefinitions=[{key:"UserPermGuestUploads",bit:1<<8,icon:"bi bi-box-arrow-in-down",title:"Create file requests",htmlId:e=>`perm_guest_upload_${e}`,apiName:"PERM_GUEST_UPLOAD"},{key:"UserPermReplaceUploads",bit:1<<0,icon:"bi bi-recycle",title:"Replace own uploads",htmlId:e=>`perm_replace_${e}`,apiName:"PERM_REPLACE"},{key:"UserPermListOtherUploads",bit:1<<1,icon:"bi bi-eye",title:"List other uploads",htmlId:e=>`perm_list_${e}`,apiName:"PERM_LIST"},{key:"UserPermEditOtherUploads",bit:1<<2,icon:"bi bi-pencil",title:"Edit other uploads",htmlId:e=>`perm_edit_${e}`,apiName:"PERM_EDIT"},{key:"UserPermDeleteOtherUploads",bit:1<<4,icon:"bi bi-trash3",title:"Delete other uploads",htmlId:e=>`perm_delete_${e}`,apiName:"PERM_DELETE"},{key:"UserPermReplaceOtherUploads",bit:1<<3,icon:"bi bi-arrow-left-right",title:"Replace other uploads",htmlId:e=>`perm_replace_other_${e}`,apiName:"PERM_REPLACE_OTHER"},{key:"UserPermManageLogs",bit:1<<5,icon:"bi bi-card-list",title:"Manage system logs",htmlId:e=>`perm_logs_${e}`,apiName:"PERM_LOGS"},{key:"UserPermManageUsers",bit:1<<7,icon:"bi bi-people",title:"Manage users",htmlId:e=>`perm_users_${e}`,apiName:"PERM_USERS"},{key:"UserPermManageApiKeys",bit:1<<6,icon:"bi bi-sliders2",title:"Manage all API keys",htmlId:e=>`perm_api_${e}`,apiName:"PERM_API"}];function hasPermission(e,t){return(e&t)!==0}function addRowUser(e,t,n){e=sanitizeUserId(e);let m=document.getElementById("usertable"),o=m.insertRow(1);o.id="row-"+e;let c=o.insertCell(0),l=o.insertCell(1),d=o.insertCell(2),u=o.insertCell(3),h=o.insertCell(4),r=o.insertCell(5);c.classList.add("newUser"),l.classList.add("newUser"),d.classList.add("newUser"),u.classList.add("newUser"),h.classList.add("newUser"),r.classList.add("newUser"),c.innerText=t,l.innerText="User",d.innerText="Never",u.innerText="0";const a=document.createElement("div");if(a.className="btn-group",a.setAttribute("role","group"),isInternalAuth){const n=document.createElement("button");n.id=`pwchange-${e}`,n.type="button",n.className="btn btn-outline-light btn-sm",n.title="Reset Password",n.onclick=()=>showResetPwModal(e,t),n.innerHTML=``,a.appendChild(n)}const s=document.createElement("button");s.id=`changeRank_${e}`,s.type="button",s.className="btn btn-outline-light btn-sm",s.title="Promote User",isAdmin?s.onclick=()=>changeRank(e,"ADMIN",`changeRank_${e}`):s.disabled=!0,s.innerHTML=``,a.appendChild(s);const i=document.createElement("button");i.id=`delete-${e}`,i.type="button",i.className="btn btn-outline-danger btn-sm",i.title="Delete",i.onclick=()=>showDeleteUserModal(e,t),i.innerHTML=``,a.appendChild(i),r.innerHTML="",r.appendChild(a),h.innerHTML=PermissionDefinitions.map(t=>{let s="perm-notgranted";hasPermission(n,t.bit)&&(s="perm-granted");const o=t.htmlId(e);let i="";return hasPermission(userPermissions,t.bit)||(i="perm-nochange"),`

Password required

+{{ if .IsPaste}} +
+{{ else }} +{{ end}}

diff --git a/internal/webserver/web/templates/html_header.tmpl b/internal/webserver/web/templates/html_header.tmpl index 8818c2ad..96fdd364 100644 --- a/internal/webserver/web/templates/html_header.tmpl +++ b/internal/webserver/web/templates/html_header.tmpl @@ -79,6 +79,7 @@

{{.PublicName}}