mirror of
https://github.com/Forceu/Gokapi.git
synced 2026-02-14 20:48:38 -06:00
Add API call and GUI option to replace content of files, API returns 404 on invalid file IDs, better handling for E2E errors
This commit is contained in:
@@ -84,7 +84,7 @@ func encryptDecrypt(key []byte, url string, doEncrypt bool) interface{} {
|
||||
// Tell the controller we have an error
|
||||
errorConstructor := js.Global().Get("Error")
|
||||
errorObject := errorConstructor.New(err.Error())
|
||||
controller.Call("error", errorObject)
|
||||
js.Global().Call("displayError", errorObject)
|
||||
return
|
||||
}
|
||||
// Read the entire stream and pass it to JavaScript
|
||||
@@ -97,7 +97,7 @@ func encryptDecrypt(key []byte, url string, doEncrypt bool) interface{} {
|
||||
// We're ignoring "EOF" however, which means the stream was done
|
||||
errorConstructor := js.Global().Get("Error")
|
||||
errorObject := errorConstructor.New(err.Error())
|
||||
controller.Call("error", errorObject)
|
||||
js.Global().Call("displayError", errorObject)
|
||||
return
|
||||
}
|
||||
if n > 0 {
|
||||
|
||||
@@ -100,6 +100,12 @@ func (p DatabaseProvider) Upgrade(currentDbVersion int) {
|
||||
for _, file := range allFiles {
|
||||
p.SaveMetaData(file)
|
||||
}
|
||||
for _, apiKey := range p.GetAllApiKeys() {
|
||||
if apiKey.HasPermissionEdit() {
|
||||
apiKey.SetPermission(models.ApiPermReplace)
|
||||
p.SaveApiKey(apiKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,13 @@ func (p DatabaseProvider) Upgrade(currentDbVersion int) {
|
||||
// Add Column LastUsedString, keeping old data
|
||||
err := p.rawSqlite(`DROP TABLE IF EXISTS "UploadStatus";`)
|
||||
helper.Check(err)
|
||||
|
||||
for _, apiKey := range p.GetAllApiKeys() {
|
||||
if apiKey.HasPermissionEdit() {
|
||||
apiKey.SetPermission(models.ApiPermReplace)
|
||||
p.SaveApiKey(apiKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -567,7 +567,7 @@ function TestAWS(button, isManual) {
|
||||
<li>Does not support download progress bar</li>
|
||||
<li>Gokapi starts without user input</li>
|
||||
<li><b>Warning:</b> Password can be read with access to Gokapi configuration</li>
|
||||
<li><b>Warning:</b> During upload temporary files containing the plaintext content may be created.</li>
|
||||
<li><b>Warning:</b> During upload, temporary files containing the plaintext content may be created.</li>
|
||||
<li><b>Warning:</b> Encryption has not been audited.</li>
|
||||
</ul></p>
|
||||
</div>
|
||||
@@ -581,7 +581,7 @@ function TestAWS(button, isManual) {
|
||||
<li>Does not support download progress bar</li>
|
||||
<li>Password cannot be read with access to Gokapi configuration</li>
|
||||
<li><span style="color:red"><b>Warning:</b></span> Gokapi requires user input to start</li>
|
||||
<li><b>Warning:</b> During upload temporary files containing the plaintext content may be created.</li>
|
||||
<li><b>Warning:</b> During upload, temporary files containing the plaintext content may be created.</li>
|
||||
<li><b>Warning:</b> Encryption has not been audited.</li>
|
||||
</ul></p>
|
||||
</div>
|
||||
@@ -598,7 +598,7 @@ function TestAWS(button, isManual) {
|
||||
<li><b>Important:</b> For remote storage, CORS settings for the bucket need to allow access from the Gokapi URL</li>
|
||||
<li><b>Warning:</b> Download might be significantly slower</li>
|
||||
<li><b>Warning:</b> Password can be read with access to Gokapi configuration</li>
|
||||
<li><b>Warning:</b> During upload temporary files containing the plaintext content may be created.</li>
|
||||
<li><b>Warning:</b> During upload, temporary files containing the plaintext content may be created.</li>
|
||||
<li><b>Warning:</b> Encryption has not been audited.</li>
|
||||
</ul></p>
|
||||
</div>
|
||||
@@ -615,7 +615,7 @@ function TestAWS(button, isManual) {
|
||||
<li><b>Important:</b> For remote storage, CORS settings for the bucket need to allow access from the Gokapi URL</li>
|
||||
<li><span style="color:red"><b>Warning:</b></span> Gokapi requires user input to start</li>
|
||||
<li><b>Warning:</b> Download might be significantly slower</li>
|
||||
<li><b>Warning:</b> During upload temporary files containing the plaintext content may be created.</li>
|
||||
<li><b>Warning:</b> During upload, temporary files containing the plaintext content may be created.</li>
|
||||
<li><b>Warning:</b> Encryption has not been audited.</li>
|
||||
</ul></p>
|
||||
</div>
|
||||
@@ -629,6 +629,7 @@ function TestAWS(button, isManual) {
|
||||
<li>Does not support deduplication</li>
|
||||
<li>Does not support hotlinks to files</li>
|
||||
<li>Does not support download progress bar</li>
|
||||
<li>Does not support replacing existing files with different content</li>
|
||||
<li>Gokapi starts without user input</li>
|
||||
<li>Files uploaded through the API have to be unencrypted</li>
|
||||
<li>Password cannot be read with access to Gokapi configuration</li>
|
||||
|
||||
@@ -13,17 +13,19 @@ const (
|
||||
ApiPermApiMod
|
||||
// ApiPermEdit is the permission for editing parameters of uploaded files
|
||||
ApiPermEdit
|
||||
// ApiPermReplace is the permission for replacing the content of uploaded files
|
||||
ApiPermReplace
|
||||
)
|
||||
|
||||
// ApiPermNone means no permission granted
|
||||
const ApiPermNone = 0
|
||||
|
||||
// ApiPermAll means all permission granted
|
||||
const ApiPermAll = 31
|
||||
const ApiPermAll = 63
|
||||
|
||||
// ApiPermAllNoApiMod means all permission granted, except ApiPermApiMod
|
||||
// ApiPermAllNoApiMod means all permission granted, except ApiPermApiMod and ApiPermReplace
|
||||
// This is the default for new API keys that are created from the UI
|
||||
const ApiPermAllNoApiMod = ApiPermAll - ApiPermApiMod
|
||||
const ApiPermAllNoApiMod = ApiPermAll - ApiPermApiMod - ApiPermReplace
|
||||
|
||||
// ApiKey contains data of a single api key
|
||||
type ApiKey struct {
|
||||
@@ -86,6 +88,11 @@ func (key *ApiKey) HasPermissionEdit() bool {
|
||||
return key.HasPermission(ApiPermEdit)
|
||||
}
|
||||
|
||||
// HasPermissionReplace returns true if ApiPermReplace is granted
|
||||
func (key *ApiKey) HasPermissionReplace() bool {
|
||||
return key.HasPermission(ApiPermReplace)
|
||||
}
|
||||
|
||||
// ApiKeyOutput is the output that is used after a new key is created
|
||||
type ApiKeyOutput struct {
|
||||
Result string
|
||||
|
||||
@@ -46,6 +46,7 @@ type FileApiOutput struct {
|
||||
UnlimitedTime bool `json:"UnlimitedTime"` // True if the uploader did not limit the time
|
||||
RequiresClientSideDecryption bool `json:"RequiresClientSideDecryption"` // True if the file has to be decrypted client-side
|
||||
IsEncrypted bool `json:"IsEncrypted"` // True if the file is encrypted
|
||||
IsEndToEndEncrypted bool `json:"IsEndToEndEncrypted"` // True if the file is end-to-end encrypted
|
||||
IsPasswordProtected bool `json:"IsPasswordProtected"` // True if a password has to be entered before downloading the file
|
||||
IsSavedOnLocalStorage bool `json:"IsSavedOnLocalStorage"` // True if the file does not use cloud storage
|
||||
}
|
||||
@@ -76,6 +77,7 @@ func (f *File) ToFileApiOutput(serverUrl string, useFilenameInUrl bool) (FileApi
|
||||
if f.Encryption.IsEndToEndEncrypted || f.RequiresClientDecryption() {
|
||||
result.RequiresClientSideDecryption = true
|
||||
}
|
||||
result.IsEndToEndEncrypted = f.Encryption.IsEndToEndEncrypted
|
||||
result.UrlHotlink = getHotlinkUrl(result, serverUrl, useFilenameInUrl)
|
||||
result.UrlDownload = getDownloadUrl(result, serverUrl, useFilenameInUrl)
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@ func TestToJsonResult(t *testing.T) {
|
||||
UnlimitedDownloads: true,
|
||||
UnlimitedTime: true,
|
||||
}
|
||||
test.IsEqualString(t, file.ToJsonResult("serverurl/", false), `{"Result":"OK","FileInfo":{"Id":"testId","Name":"testName","Size":"10 B","HotlinkId":"hotlinkid","ContentType":"text/html","ExpireAtString":"future","UrlDownload":"serverurl/d?id=testId","UrlHotlink":"","ExpireAt":50,"SizeBytes":10,"DownloadsRemaining":1,"DownloadCount":3,"UnlimitedDownloads":true,"UnlimitedTime":true,"RequiresClientSideDecryption":true,"IsEncrypted":true,"IsPasswordProtected":true,"IsSavedOnLocalStorage":false},"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":"future","UrlDownload":"serverurl/d/testId/testName","UrlHotlink":"","ExpireAt":50,"SizeBytes":10,"DownloadsRemaining":1,"DownloadCount":3,"UnlimitedDownloads":true,"UnlimitedTime":true,"RequiresClientSideDecryption":true,"IsEncrypted":true,"IsPasswordProtected":true,"IsSavedOnLocalStorage":false},"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":"future","UrlDownload":"serverurl/d?id=testId","UrlHotlink":"","ExpireAt":50,"SizeBytes":10,"DownloadsRemaining":1,"DownloadCount":3,"UnlimitedDownloads":true,"UnlimitedTime":true,"RequiresClientSideDecryption":true,"IsEncrypted":true,"IsEndToEndEncrypted":false,"IsPasswordProtected":true,"IsSavedOnLocalStorage":false},"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":"future","UrlDownload":"serverurl/d/testId/testName","UrlHotlink":"","ExpireAt":50,"SizeBytes":10,"DownloadsRemaining":1,"DownloadCount":3,"UnlimitedDownloads":true,"UnlimitedTime":true,"RequiresClientSideDecryption":true,"IsEncrypted":true,"IsEndToEndEncrypted":false,"IsPasswordProtected":true,"IsSavedOnLocalStorage":false},"IncludeFilename":true}`)
|
||||
}
|
||||
|
||||
func TestIsLocalStorage(t *testing.T) {
|
||||
|
||||
@@ -38,6 +38,12 @@ import (
|
||||
// ErrorFileTooLarge is an error that is called when a file larger than the set maximum is uploaded
|
||||
var ErrorFileTooLarge = errors.New("upload limit exceeded")
|
||||
|
||||
// ErrorReplaceE2EFile is caused when an end-to-end encrypted file is replaced
|
||||
var ErrorReplaceE2EFile = errors.New("end-to-end encrypted files cannot be replaced")
|
||||
|
||||
// ErrorFileNotFound is raised when an invalid ID is passed or the file has expired
|
||||
var ErrorFileNotFound = errors.New("file not found")
|
||||
|
||||
// NewFile creates a new file in the system. Called after an upload from the API has been completed. If a file with the same sha1 hash
|
||||
// already exists, it is deduplicated. This function gathers information about the file, creates an ID and saves
|
||||
// it into the global configuration. It is now only used by the API, the web UI uses NewFileFromChunk
|
||||
@@ -337,6 +343,35 @@ const (
|
||||
ParamName
|
||||
)
|
||||
|
||||
// ReplaceFile replaces the file content of fileId with the content of newFileContentId
|
||||
// Replacing e2e encrypted files is NOT possible
|
||||
func ReplaceFile(fileId, newFileContentId string, delete bool) (models.File, error) {
|
||||
file, ok := GetFile(fileId)
|
||||
if !ok {
|
||||
return models.File{}, ErrorFileNotFound
|
||||
}
|
||||
newFileContent, ok := GetFile(newFileContentId)
|
||||
if !ok {
|
||||
return models.File{}, ErrorFileNotFound
|
||||
}
|
||||
if file.Encryption.IsEndToEndEncrypted || newFileContent.Encryption.IsEndToEndEncrypted {
|
||||
return models.File{}, ErrorReplaceE2EFile
|
||||
}
|
||||
|
||||
file.Name = newFileContent.Name
|
||||
file.Size = newFileContent.Size
|
||||
file.SHA1 = newFileContent.SHA1
|
||||
file.ContentType = newFileContent.ContentType
|
||||
file.AwsBucket = newFileContent.AwsBucket
|
||||
file.SizeBytes = newFileContent.SizeBytes
|
||||
file.Encryption = newFileContent.Encryption
|
||||
database.SaveMetaData(file)
|
||||
if delete {
|
||||
DeleteFile(newFileContent.Id, false)
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
// DuplicateFile creates a copy of an existing file with new parameters
|
||||
func DuplicateFile(file models.File, parametersToChange int, newFileName string, fileParameters models.UploadRequest) (models.File, error) {
|
||||
var newFile models.File
|
||||
|
||||
@@ -44,6 +44,8 @@ func Process(w http.ResponseWriter, r *http.Request, maxMemory int) {
|
||||
duplicateFile(w, request)
|
||||
case "/files/modify":
|
||||
editFile(w, request)
|
||||
case "/files/replace":
|
||||
replaceFile(w, request)
|
||||
case "/auth/create":
|
||||
createApiKey(w, request)
|
||||
case "/auth/friendlyname":
|
||||
@@ -60,7 +62,7 @@ func Process(w http.ResponseWriter, r *http.Request, maxMemory int) {
|
||||
func editFile(w http.ResponseWriter, request apiRequest) {
|
||||
file, ok := database.GetMetaDataById(request.filemodInfo.id)
|
||||
if !ok {
|
||||
sendError(w, http.StatusBadRequest, "Invalid file ID provided.")
|
||||
sendError(w, http.StatusNotFound, "Invalid file ID provided.")
|
||||
return
|
||||
}
|
||||
if request.filemodInfo.downloads != "" {
|
||||
@@ -125,6 +127,8 @@ func getApiPermissionRequired(requestUrl string) (uint8, bool) {
|
||||
return models.ApiPermUpload, true
|
||||
case "/files/modify":
|
||||
return models.ApiPermEdit, true
|
||||
case "/files/replace":
|
||||
return models.ApiPermReplace, true
|
||||
case "/auth/create":
|
||||
return models.ApiPermApiMod, true
|
||||
case "/auth/friendlyname":
|
||||
@@ -200,7 +204,7 @@ func modifyApiPermission(w http.ResponseWriter, request apiRequest) {
|
||||
if !isValidKeyForEditing(w, request) {
|
||||
return
|
||||
}
|
||||
if request.apiInfo.permission < models.ApiPermView || request.apiInfo.permission > models.ApiPermEdit {
|
||||
if request.apiInfo.permission < models.ApiPermView || request.apiInfo.permission > models.ApiPermReplace {
|
||||
sendError(w, http.StatusBadRequest, "Invalid permission sent")
|
||||
return
|
||||
}
|
||||
@@ -328,10 +332,9 @@ func list(w http.ResponseWriter) {
|
||||
}
|
||||
|
||||
func listSingle(w http.ResponseWriter, id string) {
|
||||
timeNow := time.Now().Unix()
|
||||
config := configuration.Get()
|
||||
file, ok := database.GetMetaDataById(id)
|
||||
if !ok || storage.IsExpiredFile(file, timeNow) {
|
||||
file, ok := storage.GetFile(id)
|
||||
if !ok {
|
||||
sendError(w, http.StatusNotFound, "Could not find file with id "+id)
|
||||
return
|
||||
}
|
||||
@@ -365,7 +368,7 @@ func duplicateFile(w http.ResponseWriter, request apiRequest) {
|
||||
}
|
||||
file, ok := storage.GetFile(request.fileInfo.id)
|
||||
if !ok {
|
||||
sendError(w, http.StatusBadRequest, "Invalid id provided.")
|
||||
sendError(w, http.StatusNotFound, "Invalid id provided.")
|
||||
return
|
||||
}
|
||||
err = request.parseUploadRequest()
|
||||
@@ -381,6 +384,27 @@ func duplicateFile(w http.ResponseWriter, request apiRequest) {
|
||||
outputFileInfo(w, newFile)
|
||||
}
|
||||
|
||||
func replaceFile(w http.ResponseWriter, request apiRequest) {
|
||||
err := request.parseForm()
|
||||
if err != nil {
|
||||
sendError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
modifiedFile, err := storage.ReplaceFile(request.fileInfo.id, request.filemodInfo.idNewContent, request.filemodInfo.deleteNewFile)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, storage.ErrorReplaceE2EFile):
|
||||
sendError(w, http.StatusBadRequest, "End-to-End encrypted files cannot be replaced")
|
||||
case errors.Is(err, storage.ErrorFileNotFound):
|
||||
sendError(w, http.StatusNotFound, "A file with such an ID could not be found")
|
||||
default:
|
||||
sendError(w, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
outputFileInfo(w, modifiedFile)
|
||||
}
|
||||
|
||||
func outputFileInfo(w http.ResponseWriter, file models.File) {
|
||||
config := configuration.Get()
|
||||
publicOutput, err := file.ToFileApiOutput(config.ServerUrl, config.IncludeFilename)
|
||||
@@ -456,10 +480,12 @@ type apiInfo struct {
|
||||
}
|
||||
type filemodInfo struct {
|
||||
id string
|
||||
idNewContent string
|
||||
downloads string
|
||||
expiry string
|
||||
password string
|
||||
originalPassword bool
|
||||
deleteNewFile bool
|
||||
}
|
||||
|
||||
func parseRequest(r *http.Request) apiRequest {
|
||||
@@ -475,6 +501,8 @@ func parseRequest(r *http.Request) apiRequest {
|
||||
permission = models.ApiPermApiMod
|
||||
case "PERM_EDIT":
|
||||
permission = models.ApiPermEdit
|
||||
case "PERM_REPLACE":
|
||||
permission = models.ApiPermReplace
|
||||
}
|
||||
return apiRequest{
|
||||
apiKey: r.Header.Get("apikey"),
|
||||
@@ -483,10 +511,12 @@ func parseRequest(r *http.Request) apiRequest {
|
||||
fileInfo: fileInfo{id: r.Header.Get("id")},
|
||||
filemodInfo: filemodInfo{
|
||||
id: r.Header.Get("id"),
|
||||
idNewContent: r.Header.Get("idNewContent"),
|
||||
downloads: r.Header.Get("allowedDownloads"),
|
||||
expiry: r.Header.Get("expiryTimestamp"),
|
||||
password: r.Header.Get("password"),
|
||||
originalPassword: r.Header.Get("originalPassword") == "true",
|
||||
deleteNewFile: r.Header.Get("deleteOriginal") == "true",
|
||||
},
|
||||
apiInfo: apiInfo{
|
||||
friendlyName: r.Header.Get("friendlyName"),
|
||||
|
||||
@@ -96,7 +96,7 @@ func setPermissionApikey(key string, newPermission uint8, t *testing.T) {
|
||||
}
|
||||
|
||||
func getAvailablePermissions(t *testing.T) []uint8 {
|
||||
result := []uint8{models.ApiPermView, models.ApiPermUpload, models.ApiPermDelete, models.ApiPermApiMod, models.ApiPermEdit}
|
||||
result := []uint8{models.ApiPermView, models.ApiPermUpload, models.ApiPermDelete, models.ApiPermApiMod, models.ApiPermEdit, models.ApiPermReplace}
|
||||
sum := 0
|
||||
for _, perm := range result {
|
||||
sum = sum + int(perm)
|
||||
|
||||
@@ -278,6 +278,9 @@
|
||||
},
|
||||
"401": {
|
||||
"description": "Invalid API key provided"
|
||||
},
|
||||
"404": {
|
||||
"description": "Invalid ID provided or file has expired"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -292,7 +295,7 @@
|
||||
"operationId": "modifyfile",
|
||||
"security": [
|
||||
{
|
||||
"apikey": ["API_EDIT"]
|
||||
"apikey": ["EDIT"]
|
||||
},
|
||||
],
|
||||
"parameters": [
|
||||
@@ -358,6 +361,73 @@
|
||||
},
|
||||
"401": {
|
||||
"description": "Invalid API key provided"
|
||||
},
|
||||
"404": {
|
||||
"description": "Invalid ID provided or file has expired"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/files/replace": {
|
||||
"put": {
|
||||
"tags": [
|
||||
"files"
|
||||
],
|
||||
"summary": "Replaces an uploaded file",
|
||||
"description": "This API replaces the content of an uploaded file with the content of a different (already uplaoded) file. Note: Replacing end-to-end ecrypted files is NOT possible and will result in an error when downloading. Requires permission REPLACE",
|
||||
"operationId": "replacefile",
|
||||
"security": [
|
||||
{
|
||||
"apikey": ["REPLACE"]
|
||||
},
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "header",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "ID of file to be replaced"
|
||||
},{
|
||||
"name": "idNewContent",
|
||||
"in": "header",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "ID of the file with the new content"
|
||||
},
|
||||
{
|
||||
"name": "deleteNewFile",
|
||||
"in": "header",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": "If true, the file with the ID passed in idNewContent will be deleted afterwards"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Operation successful",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/File"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid ID supplied or incorrect data type sent"
|
||||
},
|
||||
"401": {
|
||||
"description": "Invalid API key provided"
|
||||
},
|
||||
"404": {
|
||||
"description": "Invalid ID provided or file has expired"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -697,6 +767,11 @@
|
||||
"type": "boolean",
|
||||
"example": "false"
|
||||
},
|
||||
"IsEndToEndEncrypted": {
|
||||
"description": "True if the file is end-to-end encrypted",
|
||||
"type": "boolean",
|
||||
"example": "false"
|
||||
},
|
||||
"IsPasswordProtected": {
|
||||
"type": "boolean",
|
||||
"description": "True if a password has to be entered before downloading the file",
|
||||
|
||||
@@ -340,10 +340,62 @@ function showError(file, message) {
|
||||
}
|
||||
|
||||
|
||||
async function apiEdit(id, allowedDownloads, expiry, password, originalPw) {
|
||||
let apiUrl = './api/files/modify';
|
||||
|
||||
const requestOptions = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'id': id,
|
||||
'apikey': systemKey,
|
||||
'allowedDownloads': allowedDownloads,
|
||||
'expiryTimestamp': expiry,
|
||||
'password': password,
|
||||
'originalPassword': originalPw
|
||||
},
|
||||
};
|
||||
await fetch(apiUrl, requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request failed with status: ${response.status}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
throw error
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function apiReplace(id, newId) {
|
||||
let apiUrl = './api/files/replace';
|
||||
|
||||
const requestOptions = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'id': id,
|
||||
'apikey': systemKey,
|
||||
'idNewContent': newId,
|
||||
'deleteNewFile': false
|
||||
},
|
||||
};
|
||||
await fetch(apiUrl, requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request failed with status: ${response.status}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
throw error
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function editFile() {
|
||||
const button = document.getElementById('mb_save');
|
||||
button.disabled = true;
|
||||
let apiUrl = './api/files/modify';
|
||||
let id = button.getAttribute('data-fileid');
|
||||
|
||||
let allowedDownloads = document.getElementById('mi_edit_down').value;
|
||||
let expiryTimestamp = document.getElementById('mi_edit_expiry').value;
|
||||
@@ -361,29 +413,28 @@ function editFile() {
|
||||
password = "";
|
||||
}
|
||||
|
||||
const requestOptions = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'id': button.getAttribute('data-fileid'),
|
||||
'apikey': systemKey,
|
||||
'allowedDownloads': allowedDownloads,
|
||||
'expiryTimestamp': expiryTimestamp,
|
||||
'password': password,
|
||||
'originalPassword': originalPassword
|
||||
let replaceFile = false;
|
||||
let replaceId = "";
|
||||
if (document.getElementById('mc_replace').checked) {
|
||||
replaceFile = true;
|
||||
replaceId = document.getElementById('mi_edit_replace').value;
|
||||
}
|
||||
|
||||
},
|
||||
};
|
||||
|
||||
// Send the request
|
||||
fetch(apiUrl, requestOptions)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request failed with status: ${response.status}`);
|
||||
}
|
||||
})
|
||||
apiEdit(id, allowedDownloads, expiryTimestamp, password, originalPassword)
|
||||
.then(data => {
|
||||
location.reload();
|
||||
if (!replaceFile) {
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
apiReplace(id, replaceId)
|
||||
.then(data => {
|
||||
location.reload();
|
||||
})
|
||||
.catch(error => {
|
||||
alert("Unable to edit file: " + error);
|
||||
console.error('Error:', error);
|
||||
button.disabled = false;
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
alert("Unable to edit file: " + error);
|
||||
@@ -433,7 +484,7 @@ function handleEditCheckboxChange(checkbox) {
|
||||
|
||||
}
|
||||
|
||||
function showEditModal(filename, id, downloads, expiry, password, unlimitedown, unlimitedtime) {
|
||||
function showEditModal(filename, id, downloads, expiry, password, unlimitedown, unlimitedtime, isE2e) {
|
||||
document.getElementById("m_filenamelabel").innerHTML = filename;
|
||||
document.getElementById("mc_expiry").setAttribute("data-timestamp", expiry);
|
||||
document.getElementById("mb_save").setAttribute('data-fileid', id);
|
||||
@@ -470,6 +521,24 @@ function showEditModal(filename, id, downloads, expiry, password, unlimitedown,
|
||||
document.getElementById("mi_edit_pw").disabled = true;
|
||||
document.getElementById("mc_password").checked = false;
|
||||
}
|
||||
|
||||
let selectReplace = document.getElementById("mi_edit_replace");
|
||||
if (!isE2e) {
|
||||
let files = getAllAvailableFiles();
|
||||
for (let i = 0; i < files[0].length; i++) {
|
||||
if (files[0][i] == id)
|
||||
continue;
|
||||
selectReplace.add(new Option(files[1][i] + " (" + files[0][i] + ")", files[0][i]));
|
||||
}
|
||||
} else {
|
||||
document.getElementById("mc_replace").disabled = true;
|
||||
document.getElementById("mc_replace").title = "Replacing content is not available for end-to-end encrypted files";
|
||||
selectReplace.add(new Option("Unavailable", 0));
|
||||
selectReplace.title = "Replacing content is not available for end-to-end encrypted files";
|
||||
selectReplace.value = "0";
|
||||
}
|
||||
|
||||
|
||||
new bootstrap.Modal('#modaledit', {}).show();
|
||||
}
|
||||
|
||||
@@ -490,6 +559,18 @@ function add14DaysIfBeforeCurrentTime(unixTimestamp) {
|
||||
}
|
||||
}
|
||||
|
||||
function getAllAvailableFiles() {
|
||||
let ids = [];
|
||||
let filenames = [];
|
||||
|
||||
let elements = document.querySelectorAll('[id^="cell-name-"]');
|
||||
for (let element of elements) {
|
||||
ids.push(element.id.replace("cell-name-", ""));
|
||||
filenames.push(element.innerHTML);
|
||||
}
|
||||
return [ids, filenames];
|
||||
}
|
||||
|
||||
function changeApiPermission(apiKey, permission, buttonId) {
|
||||
|
||||
var indicator = document.getElementById(buttonId);
|
||||
@@ -807,7 +888,7 @@ function addRow(item) {
|
||||
buttons = buttons + '<button type="button" onclick="showToast()" data-clipboard-text="' + item.UrlHotlink + '" class="copyurl btn btn-outline-light btn-sm"><i class="bi bi-copy"></i> Hotlink</button> ';
|
||||
}
|
||||
buttons = buttons + '<button type="button" id="qrcode-' + item.Id + '" title="QR Code" class="btn btn-outline-light btn-sm" onclick="showQrCode(\'' + item.UrlDownload + '\');"><i class="bi bi-qr-code"></i></button> ';
|
||||
buttons = buttons + '<button type="button" title="Edit" class="btn btn-outline-light btn-sm" onclick="showEditModal(\'' + item.Name + '\',\'' + item.Id + '\', ' + item.DownloadsRemaining + ', ' + item.ExpireAt + ', ' + item.IsPasswordProtected + ', ' + item.UnlimitedDownloads + ', ' + item.UnlimitedTime + ');"><i class="bi bi-pencil"></i></button> ';
|
||||
buttons = buttons + '<button type="button" title="Edit" class="btn btn-outline-light btn-sm" onclick="showEditModal(\'' + item.Name + '\',\'' + item.Id + '\', ' + item.DownloadsRemaining + ', ' + item.ExpireAt + ', ' + item.IsPasswordProtected + ', ' + item.UnlimitedDownloads + ', ' + item.UnlimitedTime + ', ' + item.IsEndToEndEncrypted + ');"><i class="bi bi-pencil"></i></button> ';
|
||||
buttons = buttons + '<button type="button" id="button-delete-' + item.Id + '" title="Delete" class="btn btn-outline-danger btn-sm" onclick="deleteFile(\'' + item.Id + '\')"><i class="bi bi-trash3"></i></button>';
|
||||
|
||||
cellButtons.innerHTML = buttons;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -86,7 +86,7 @@
|
||||
<button type="button"class="copyurl btn btn-outline-light btn-sm disabled"><i class="bi bi-copy"></i> Hotlink</button>
|
||||
{{ end }}
|
||||
<button type="button" id="qrcode-{{ .Id }}" title="QR Code" class="btn btn-outline-light btn-sm" onclick="showQrCode('{{ .UrlDownload }}');"><i class="bi bi-qr-code"></i></button>
|
||||
<button type="button" title="Edit" class="btn btn-outline-light btn-sm" onclick="showEditModal('{{.Name }}','{{.Id}}', {{.DownloadsRemaining }}, {{.ExpireAt }}, {{.IsPasswordProtected}}, {{.UnlimitedDownloads }}, {{.UnlimitedTime}});"><i class="bi bi-pencil"></i></button>
|
||||
<button type="button" title="Edit" class="btn btn-outline-light btn-sm" onclick="showEditModal('{{.Name }}','{{.Id}}', {{.DownloadsRemaining }}, {{.ExpireAt }}, {{.IsPasswordProtected}}, {{.UnlimitedDownloads }}, {{.UnlimitedTime}}, {{.IsEndToEndEncrypted}});"><i class="bi bi-pencil"></i></button>
|
||||
<button id="button-delete-{{ .Id }}" type="button" title="Delete" class="btn btn-outline-danger btn-sm" onclick="deleteFile('{{ .Id }}')"><i class="bi bi-trash3"></i></button></td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
@@ -102,7 +102,7 @@
|
||||
|
||||
<!-- Modal for editing-->
|
||||
<div class="modal fade" id="modaledit" tabindex="-1" aria-labelledby="m_filenamelabel" aria-hidden="true">
|
||||
<div class="modal-dialog gokapi-dialog">
|
||||
<div class="modal-dialog modal-lg gokapi-dialog">
|
||||
<div class="modal-content gokapi-dialog">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="m_filenamelabel">Filename</h1>
|
||||
@@ -129,9 +129,18 @@
|
||||
<span class="input-group-text" id="edit_pw">Password </span>
|
||||
<input type="text" id="mi_edit_pw" class="form-control" aria-label="Password" disabled onclick="selectTextForPw(this)" aria-describedby="edit_pw">
|
||||
</div>
|
||||
|
||||
<div class="input-group mb-3">
|
||||
<div class="input-group-text">
|
||||
<input type="checkbox" id="mc_replace" aria-label="Replace file content" title="Replace file content" data-toggle-target="mi_edit_replace" onchange="handleEditCheckboxChange(this)">
|
||||
</div>
|
||||
<span class="input-group-text" id="edit_replace">Replace Content </span>
|
||||
<select id="mi_edit_replace" class="form-select" aria-label="Replace File Content" disabled>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-light" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-outline-light" aria-label="Close" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" data-fileid="" id="mb_save" onclick="editFile();">Save changes</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -178,6 +187,7 @@
|
||||
registerChangeHandler();
|
||||
var systemKey = "{{.SystemKey}}";
|
||||
setUploadDefaults();
|
||||
|
||||
</script>
|
||||
|
||||
{{ if .EndToEndEncryption }}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<i id="perm_upload_{{ .Id }}" class="bi bi-file-earmark-arrow-up {{if not .HasPermissionUpload}}apiperm-notgranted{{else}}apiperm-granted{{end}}" title="Upload" onclick='changeApiPermission("{{ .Id }}","PERM_UPLOAD", "perm_upload_{{ .Id }}");'></i>
|
||||
<i id="perm_edit_{{ .Id }}" class="bi bi-pencil {{if not .HasPermissionEdit}}apiperm-notgranted{{else}}apiperm-granted{{end}}" title="Edit Uploads" onclick='changeApiPermission("{{ .Id }}","PERM_EDIT", "perm_edit_{{ .Id }}");'></i>
|
||||
<i id="perm_delete_{{ .Id }}" class="bi bi-trash3 {{if not .HasPermissionDelete}}apiperm-notgranted{{else}}apiperm-granted{{end}}" title="Delete Uploads" onclick='changeApiPermission("{{ .Id }}","PERM_DELETE", "perm_delete_{{ .Id }}");'></i>
|
||||
<i id="perm_replace_{{ .Id }}" class="bi bi-recycle {{if not .HasPermissionReplace}}apiperm-notgranted{{else}}apiperm-granted{{end}}" title="Replace Uploads" onclick='changeApiPermission("{{ .Id }}","PERM_REPLACE", "perm_replace_{{ .Id }}");'></i>
|
||||
<i id="perm_api_{{ .Id }}" class="bi bi-sliders2 {{if not .HasPermissionApiMod}}apiperm-notgranted{{else}}apiperm-granted{{end}}" title="Manage API Keys" onclick='changeApiPermission("{{ .Id }}","PERM_API_MOD", "perm_api_{{ .Id }}");'></i>
|
||||
|
||||
</td>
|
||||
|
||||
@@ -133,8 +133,12 @@
|
||||
|
||||
function Download(button) {
|
||||
button.disabled = true;
|
||||
DownloadEncrypted();
|
||||
DownloadEncrypted().catch(err => {
|
||||
console.error('Error during download:', err);
|
||||
displayError(err);
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
{{ else }}
|
||||
|
||||
|
||||
77
openapi.json
77
openapi.json
@@ -278,6 +278,9 @@
|
||||
},
|
||||
"401": {
|
||||
"description": "Invalid API key provided"
|
||||
},
|
||||
"404": {
|
||||
"description": "Invalid ID provided or file has expired"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -292,7 +295,7 @@
|
||||
"operationId": "modifyfile",
|
||||
"security": [
|
||||
{
|
||||
"apikey": ["API_EDIT"]
|
||||
"apikey": ["EDIT"]
|
||||
},
|
||||
],
|
||||
"parameters": [
|
||||
@@ -358,6 +361,73 @@
|
||||
},
|
||||
"401": {
|
||||
"description": "Invalid API key provided"
|
||||
},
|
||||
"404": {
|
||||
"description": "Invalid ID provided or file has expired"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/files/replace": {
|
||||
"put": {
|
||||
"tags": [
|
||||
"files"
|
||||
],
|
||||
"summary": "Replaces an uploaded file",
|
||||
"description": "This API replaces the content of an uploaded file with the content of a different (already uplaoded) file. Note: Replacing end-to-end ecrypted files is NOT possible and will result in an error when downloading. Requires permission REPLACE",
|
||||
"operationId": "replacefile",
|
||||
"security": [
|
||||
{
|
||||
"apikey": ["REPLACE"]
|
||||
},
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "header",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "ID of file to be replaced"
|
||||
},{
|
||||
"name": "idNewContent",
|
||||
"in": "header",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "ID of the file with the new content"
|
||||
},
|
||||
{
|
||||
"name": "deleteNewFile",
|
||||
"in": "header",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": "If true, the file with the ID passed in idNewContent will be deleted afterwards"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Operation successful",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/File"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid ID supplied or incorrect data type sent"
|
||||
},
|
||||
"401": {
|
||||
"description": "Invalid API key provided"
|
||||
},
|
||||
"404": {
|
||||
"description": "Invalid ID provided or file has expired"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -697,6 +767,11 @@
|
||||
"type": "boolean",
|
||||
"example": "false"
|
||||
},
|
||||
"IsEndToEndEncrypted": {
|
||||
"description": "True if the file is end-to-end encrypted",
|
||||
"type": "boolean",
|
||||
"example": "false"
|
||||
},
|
||||
"IsPasswordProtected": {
|
||||
"type": "boolean",
|
||||
"description": "True if a password has to be entered before downloading the file",
|
||||
|
||||
Reference in New Issue
Block a user