Skip to content

Commit d834091

Browse files
authored
Add restore feature when deleting file from admin menu or API (#261)
* Added API call /files/restore, added parameter to /files/delete to add delay, have 10s delay when deleting from UI * Fixed DB upgrade function not exiting on old version, added and fixed tests
1 parent 20ee8cb commit d834091

File tree

23 files changed

+577
-86
lines changed

23 files changed

+577
-86
lines changed

internal/configuration/database/Database_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ func TestMetaData(t *testing.T) {
221221
AwsBucket: "aws1",
222222
ExpireAtString: "In 10 seconds",
223223
ExpireAt: time.Now().Add(10 * time.Second).Unix(),
224+
PendingDeletion: time.Now().Add(8 * time.Second).Unix(),
224225
UploadDate: time.Now().Unix(),
225226
SizeBytes: 3 * 1024,
226227
DownloadsRemaining: 2,

internal/configuration/database/provider/redis/Redis.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/forceu/gokapi/internal/helper"
77
"github.com/forceu/gokapi/internal/models"
88
redigo "github.com/gomodule/redigo/redis"
9+
"os"
910
"strconv"
1011
"strings"
1112
"time"
@@ -95,6 +96,8 @@ func (p DatabaseProvider) Upgrade(currentDbVersion int) {
9596
// < v1.9.6
9697
if currentDbVersion < 3 {
9798
fmt.Println("Please update to v1.9.6 before upgrading to 2.0.0")
99+
osExit(1)
100+
return
98101
}
99102
// < v2.0.0-beta1
100103
if currentDbVersion < 4 {
@@ -336,3 +339,5 @@ func (p DatabaseProvider) runEval(cmd string) {
336339
func (p DatabaseProvider) deleteAllWithPrefix(prefix string) {
337340
p.runEval("for _,k in ipairs(redis.call('keys','" + p.dbPrefix + prefix + "*')) do redis.call('del',k) end")
338341
}
342+
343+
var osExit = os.Exit

internal/configuration/database/provider/sqlite/Sqlite.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type DatabaseProvider struct {
2020
}
2121

2222
// DatabaseSchemeVersion contains the version number to be expected from the current database. If lower, an upgrade will be performed
23-
const DatabaseSchemeVersion = 9
23+
const DatabaseSchemeVersion = 10
2424

2525
// New returns an instance
2626
func New(dbConfig models.DbConnection) (DatabaseProvider, error) {
@@ -37,6 +37,8 @@ func (p DatabaseProvider) Upgrade(currentDbVersion int) {
3737
// < v1.9.6
3838
if currentDbVersion < 6 {
3939
fmt.Println("Please update to v1.9.6 before upgrading to 2.0.0")
40+
osExit(1)
41+
return
4042
}
4143
// < v2.0.0-beta
4244
if currentDbVersion < 7 {
@@ -95,6 +97,11 @@ func (p DatabaseProvider) Upgrade(currentDbVersion int) {
9597
err := p.rawSqlite(`ALTER TABLE "FileMetaData" ADD COLUMN UploadDate INTEGER NOT NULL DEFAULT 0;`)
9698
helper.Check(err)
9799
}
100+
// < v2.0.0-beta3
101+
if currentDbVersion < 10 {
102+
err := p.rawSqlite(`ALTER TABLE "FileMetaData" ADD COLUMN PendingDeletion INTEGER NOT NULL DEFAULT 0;`)
103+
helper.Check(err)
104+
}
98105
}
99106

100107
func getLegacyE2EConfig(p DatabaseProvider) models.E2EInfoEncrypted {
@@ -223,6 +230,7 @@ func (p DatabaseProvider) createNewDatabase() error {
223230
"UnlimitedTime" INTEGER NOT NULL,
224231
"UserId" INTEGER NOT NULL,
225232
"UploadDate" INTEGER NOT NULL,
233+
"PendingDeletion" INTEGER NOT NULL,
226234
PRIMARY KEY("Id")
227235
);
228236
CREATE TABLE "Hotlinks" (
@@ -264,3 +272,5 @@ func (p DatabaseProvider) rawSqlite(statement string) error {
264272
_, err := p.sqliteDb.Exec(statement)
265273
return err
266274
}
275+
276+
var osExit = os.Exit

internal/configuration/database/provider/sqlite/Sqlite_test.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -660,12 +660,23 @@ func TestDatabaseProvider_Upgrade(t *testing.T) {
660660
DROP TABLE IF EXISTS Users;
661661
DROP TABLE IF EXISTS UploadConfig;`)
662662
test.IsNil(t, err)
663-
sqliteInit, version := getSqlInitV6()
663+
sqliteInit := getSqlInitV6()
664664
err = instance.rawSqlite(sqliteInit)
665665
test.IsNil(t, err)
666-
dbInstance.SetDbVersion(version)
667666

668-
dbInstance.Upgrade(DatabaseSchemeVersion)
667+
exitCode := 0
668+
osExit = func(code int) {
669+
exitCode = code
670+
}
671+
instance.SetDbVersion(5)
672+
instance.Upgrade(instance.GetDbVersion())
673+
test.IsEqualInt(t, exitCode, 1)
674+
675+
exitCode = 0
676+
instance.SetDbVersion(6)
677+
instance.Upgrade(instance.GetDbVersion())
678+
test.IsEqualInt(t, exitCode, 0)
679+
669680
}
670681

671682
func TestRawSql(t *testing.T) {
@@ -675,7 +686,7 @@ func TestRawSql(t *testing.T) {
675686
_ = dbInstance.rawSqlite("Select * from Sessions")
676687
}
677688

678-
func getSqlInitV6() (string, int) {
689+
func getSqlInitV6() string {
679690
return `CREATE TABLE IF NOT EXISTS "ApiKeys" (
680691
"Id" TEXT NOT NULL UNIQUE,
681692
"FriendlyName" TEXT NOT NULL,
@@ -726,5 +737,5 @@ INSERT INTO "E2EConfig" VALUES (1,X'537f03010110453245496e666f456e63727970746564
726737
INSERT INTO "FileMetaData" VALUES ('M3dEz99HKN9sOgU','kodi_crashlog-20241106_102509.log','131.6 kB','0e9c019ec2698587cc973a9ee368713eb77e4fae',1737412393,134794,'2025-01-20 23:33',10,0,'','','text/x-log','',X'5f7f0301010e456e6372797074696f6e496e666f01ff80000104010b4973456e6372797074656401020001134973456e64546f456e64456e63727970746564010200010d44656372797074696f6e4b6579010a0001054e6f6e6365010a00000003ff8000',0,0);
727738
INSERT INTO "FileMetaData" VALUES ('b5Mf07AgTkwqpW2','Encrypted File','131.6 kB','e2e-ivCiN4YePueE1PcjYirB',1737412472,134938,'2025-01-20 23:34',10,0,'','','application/octet-stream','',X'60ff830301010e456e6372797074696f6e496e666f01ff84000104010b4973456e6372797074656401020001134973456e64546f456e64456e63727970746564010200010d44656372797074696f6e4b6579010a0001054e6f6e6365010a00000007ff840101010100',0,0);
728739
INSERT INTO "Hotlinks" VALUES ('Phie2AiW2aecaecahWoo','jun9keeNokae9iehinee');
729-
INSERT INTO "Sessions" VALUES ('zMUYkok9UZZiKBCHB5pO7KPTPzPP71ashpRf11W37wP0HMhMjTKcFL8Ai6Z3',173624606799,173879486799);`, 6
740+
INSERT INTO "Sessions" VALUES ('zMUYkok9UZZiKBCHB5pO7KPTPzPP71ashpRf11W37wP0HMhMjTKcFL8Ai6Z3',173624606799,173879486799);`
730741
}

internal/configuration/database/provider/sqlite/metadata.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type schemaMetaData struct {
2828
UnlimitedTime int
2929
UserId int
3030
UploadDate int64
31+
PendingDeletion int64
3132
}
3233

3334
func (rowData schemaMetaData) ToFileModel() (models.File, error) {
@@ -50,6 +51,7 @@ func (rowData schemaMetaData) ToFileModel() (models.File, error) {
5051
UnlimitedTime: rowData.UnlimitedTime == 1,
5152
UserId: rowData.UserId,
5253
UploadDate: rowData.UploadDate,
54+
PendingDeletion: rowData.PendingDeletion,
5355
}
5456

5557
buf := bytes.NewBuffer(rowData.Encryption)
@@ -69,7 +71,7 @@ func (p DatabaseProvider) GetAllMetadata() map[string]models.File {
6971
err = rows.Scan(&rowData.Id, &rowData.Name, &rowData.Size, &rowData.SHA1, &rowData.ExpireAt, &rowData.SizeBytes,
7072
&rowData.ExpireAtString, &rowData.DownloadsRemaining, &rowData.DownloadCount, &rowData.PasswordHash,
7173
&rowData.HotlinkId, &rowData.ContentType, &rowData.AwsBucket, &rowData.Encryption,
72-
&rowData.UnlimitedDownloads, &rowData.UnlimitedTime, &rowData.UserId, &rowData.UploadDate)
74+
&rowData.UnlimitedDownloads, &rowData.UnlimitedTime, &rowData.UserId, &rowData.UploadDate, &rowData.PendingDeletion)
7375
helper.Check(err)
7476
var metaData models.File
7577
metaData, err = rowData.ToFileModel()
@@ -103,7 +105,7 @@ func (p DatabaseProvider) GetMetaDataById(id string) (models.File, bool) {
103105
err := row.Scan(&rowData.Id, &rowData.Name, &rowData.Size, &rowData.SHA1, &rowData.ExpireAt, &rowData.SizeBytes,
104106
&rowData.ExpireAtString, &rowData.DownloadsRemaining, &rowData.DownloadCount, &rowData.PasswordHash,
105107
&rowData.HotlinkId, &rowData.ContentType, &rowData.AwsBucket, &rowData.Encryption,
106-
&rowData.UnlimitedDownloads, &rowData.UnlimitedTime, &rowData.UserId, &rowData.UploadDate)
108+
&rowData.UnlimitedDownloads, &rowData.UnlimitedTime, &rowData.UserId, &rowData.UploadDate, &rowData.PendingDeletion)
107109
if err != nil {
108110
if errors.Is(err, sql.ErrNoRows) {
109111
return result, false
@@ -134,6 +136,7 @@ func (p DatabaseProvider) SaveMetaData(file models.File) {
134136
AwsBucket: file.AwsBucket,
135137
UserId: file.UserId,
136138
UploadDate: file.UploadDate,
139+
PendingDeletion: file.PendingDeletion,
137140
}
138141

139142
if file.UnlimitedDownloads {
@@ -151,10 +154,11 @@ func (p DatabaseProvider) SaveMetaData(file models.File) {
151154

152155
_, err = p.sqliteDb.Exec(`INSERT OR REPLACE INTO FileMetaData (Id, Name, Size, SHA1, ExpireAt, SizeBytes, ExpireAtString,
153156
DownloadsRemaining, DownloadCount, PasswordHash, HotlinkId, ContentType, AwsBucket, Encryption,
154-
UnlimitedDownloads, UnlimitedTime, UserId, UploadDate) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
157+
UnlimitedDownloads, UnlimitedTime, UserId, UploadDate, PendingDeletion)
158+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
155159
newData.Id, newData.Name, newData.Size, newData.SHA1, newData.ExpireAt, newData.SizeBytes, newData.ExpireAtString,
156160
newData.DownloadsRemaining, newData.DownloadCount, newData.PasswordHash, newData.HotlinkId, newData.ContentType,
157-
newData.AwsBucket, newData.Encryption, newData.UnlimitedDownloads, newData.UnlimitedTime, newData.UserId, newData.UploadDate)
161+
newData.AwsBucket, newData.Encryption, newData.UnlimitedDownloads, newData.UnlimitedTime, newData.UserId, newData.UploadDate, newData.PendingDeletion)
158162
helper.Check(err)
159163
}
160164

internal/logging/Logging.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@ func LogDelete(file models.File, user models.User) {
131131
createLogEntry(categoryEdit, fmt.Sprintf("%s, ID %s, deleted by %s (user #%d)", file.Name, file.Id, user.Name, user.Id), false)
132132
}
133133

134+
// LogRestore adds a log entry when the pending deletion of a file was cancelled and the file restored. Non-Blocking
135+
func LogRestore(file models.File, user models.User) {
136+
createLogEntry(categoryEdit, fmt.Sprintf("%s, ID %s, restored by %s (user #%d)", file.Name, file.Id, user.Name, user.Id), false)
137+
}
138+
134139
// UpgradeToV2 adds tags to existing logs
135140
// deprecated
136141
func UpgradeToV2() {

internal/models/FileList.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ type File struct {
1313
Name string `json:"Name" redis:"Name"` // The filename. Will be 'Encrypted file' for end-to-end encrypted files
1414
Size string `json:"Size" redis:"Size"` // Filesize in a human-readable format
1515
SHA1 string `json:"SHA1" redis:"SHA1"` // The hash of the file, used for deduplication
16-
PasswordHash string `json:"PasswordHash" redis:"PasswordHash"` // The hash of the password (if the file is password protected)
16+
PasswordHash string `json:"PasswordHash" redis:"PasswordHash"` // The hash of the password (if the file is password-protected)
1717
HotlinkId string `json:"HotlinkId" redis:"HotlinkId"` // If file is a picture file and can be hotlinked, this is the ID for the hotlink
1818
ContentType string `json:"ContentType" redis:"ContentType"` // The MIME type for the file
1919
AwsBucket string `json:"AwsBucket" redis:"AwsBucket"` // If the file is stored in the cloud, this is the bucket that is being used
2020
ExpireAtString string `json:"ExpireAtString" redis:"ExpireAtString"` // Time expiry in a human-readable format in local time
2121
ExpireAt int64 `json:"ExpireAt" redis:"ExpireAt"` // UTC timestamp of file expiry
22+
PendingDeletion int64 `json:"PendingDeletion" redis:"PendingDeletion"` // UTC timestamp when the file will be deleted, if pending. Otherwise 0
2223
SizeBytes int64 `json:"SizeBytes" redis:"SizeBytes"` // Filesize in bytes
2324
UploadDate int64 `json:"UploadDate" redis:"UploadDate"` // UTC timestamp of upload time
2425
DownloadsRemaining int `json:"DownloadsRemaining" redis:"DownloadsRemaining"` // The remaining downloads for this file
@@ -35,23 +36,24 @@ type FileApiOutput struct {
3536
Id string `json:"Id"` // The internal ID of the file
3637
Name string `json:"Name"` // The filename. Will be 'Encrypted file' for end-to-end encrypted files
3738
Size string `json:"Size"` // Filesize in a human-readable format
38-
HotlinkId string `json:"HotlinkId"` // If file is a picture file and can be hotlinked, this is the ID for the hotlink
39+
HotlinkId string `json:"HotlinkId"` // If the file is a picture file and can be hotlinked, this is the ID for the hotlink
3940
ContentType string `json:"ContentType"` // The MIME type for the file
4041
ExpireAtString string `json:"ExpireAtString"` // Time expiry in a human-readable format in local time
4142
UrlDownload string `json:"UrlDownload"` // The public download URL for the file
4243
UrlHotlink string `json:"UrlHotlink"` // The public hotlink URL for the file
4344
UploadDate int64 `json:"UploadDate"` // UTC timestamp of upload time
44-
ExpireAt int64 `json:"ExpireAt"` // "UTC timestamp of file expiry
45+
ExpireAt int64 `json:"ExpireAt"` // UTC timestamp of file expiry
4546
SizeBytes int64 `json:"SizeBytes"` // Filesize in bytes
4647
DownloadsRemaining int `json:"DownloadsRemaining"` // The remaining downloads for this file
47-
DownloadCount int `json:"DownloadCount"` // The amount of times the file has been downloaded
48+
DownloadCount int `json:"DownloadCount"` // The number of times the file has been downloaded
4849
UnlimitedDownloads bool `json:"UnlimitedDownloads"` // True if the uploader did not limit the downloads
4950
UnlimitedTime bool `json:"UnlimitedTime"` // True if the uploader did not limit the time
5051
RequiresClientSideDecryption bool `json:"RequiresClientSideDecryption"` // True if the file has to be decrypted client-side
5152
IsEncrypted bool `json:"IsEncrypted"` // True if the file is encrypted
5253
IsEndToEndEncrypted bool `json:"IsEndToEndEncrypted"` // True if the file is end-to-end encrypted
5354
IsPasswordProtected bool `json:"IsPasswordProtected"` // True if a password has to be entered before downloading the file
5455
IsSavedOnLocalStorage bool `json:"IsSavedOnLocalStorage"` // True if the file does not use cloud storage
56+
IsPendingDeletion bool `json:"IsPendingDeletion"` // True if the file is about to be deleted
5557
UploaderId int `json:"UploaderId"` // The user ID of the uploader
5658
}
5759

@@ -68,7 +70,12 @@ func (f *File) IsLocalStorage() bool {
6870
return f.AwsBucket == ""
6971
}
7072

71-
// ToFileApiOutput returns a json object without sensitive information
73+
// IsPendingForDeletion returns true if the file is pending to be deleted
74+
func (f *File) IsPendingForDeletion() bool {
75+
return f.PendingDeletion != 0
76+
}
77+
78+
// ToFileApiOutput returns a JSON object without sensitive information
7279
func (f *File) ToFileApiOutput(serverUrl string, useFilenameInUrl bool) (FileApiOutput, error) {
7380
var result FileApiOutput
7481
err := copier.Copy(&result, &f)
@@ -85,6 +92,7 @@ func (f *File) ToFileApiOutput(serverUrl string, useFilenameInUrl bool) (FileApi
8592
result.UrlHotlink = getHotlinkUrl(result, serverUrl, useFilenameInUrl)
8693
result.UrlDownload = getDownloadUrl(result, serverUrl, useFilenameInUrl)
8794
result.UploaderId = f.UserId
95+
result.IsPendingDeletion = f.IsPendingForDeletion()
8896

8997
return result, nil
9098
}

internal/models/FileList_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ func TestToJsonResult(t *testing.T) {
3030
},
3131
UnlimitedDownloads: true,
3232
UnlimitedTime: true,
33+
PendingDeletion: 100,
3334
}
34-
test.IsEqualString(t, file.ToJsonResult("serverurl/", false), `{"Result":"OK","FileInfo":{"Id":"testId","Name":"testName","Size":"10 B","HotlinkId":"hotlinkid","ContentType":"text/html","ExpireAtString":"Wed Jun 25 2025 11:48:28","UrlDownload":"serverurl/d?id=testId","UrlHotlink":"","UploadDate":1748180908,"ExpireAt":1750852108,"SizeBytes":10,"DownloadsRemaining":1,"DownloadCount":3,"UnlimitedDownloads":true,"UnlimitedTime":true,"RequiresClientSideDecryption":true,"IsEncrypted":true,"IsEndToEndEncrypted":false,"IsPasswordProtected":true,"IsSavedOnLocalStorage":false,"UploaderId":2},"IncludeFilename":false}`)
35-
test.IsEqualString(t, file.ToJsonResult("serverurl/", true), `{"Result":"OK","FileInfo":{"Id":"testId","Name":"testName","Size":"10 B","HotlinkId":"hotlinkid","ContentType":"text/html","ExpireAtString":"Wed Jun 25 2025 11:48:28","UrlDownload":"serverurl/d/testId/testName","UrlHotlink":"","UploadDate":1748180908,"ExpireAt":1750852108,"SizeBytes":10,"DownloadsRemaining":1,"DownloadCount":3,"UnlimitedDownloads":true,"UnlimitedTime":true,"RequiresClientSideDecryption":true,"IsEncrypted":true,"IsEndToEndEncrypted":false,"IsPasswordProtected":true,"IsSavedOnLocalStorage":false,"UploaderId":2},"IncludeFilename":true}`)
35+
test.IsEqualString(t, file.ToJsonResult("serverurl/", false), `{"Result":"OK","FileInfo":{"Id":"testId","Name":"testName","Size":"10 B","HotlinkId":"hotlinkid","ContentType":"text/html","ExpireAtString":"Wed Jun 25 2025 11:48:28","UrlDownload":"serverurl/d?id=testId","UrlHotlink":"","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,"UploaderId":2},"IncludeFilename":false}`)
36+
test.IsEqualString(t, file.ToJsonResult("serverurl/", true), `{"Result":"OK","FileInfo":{"Id":"testId","Name":"testName","Size":"10 B","HotlinkId":"hotlinkid","ContentType":"text/html","ExpireAtString":"Wed Jun 25 2025 11:48:28","UrlDownload":"serverurl/d/testId/testName","UrlHotlink":"","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,"UploaderId":2},"IncludeFilename":true}`)
3637
}
3738

3839
func TestIsLocalStorage(t *testing.T) {

0 commit comments

Comments
 (0)