Skip to content

Commit 6d68134

Browse files
authored
Added upload timestamp to metadata and API output, sort by upload data in main menu (#260)
1 parent 427ce0d commit 6d68134

File tree

15 files changed

+106
-54
lines changed

15 files changed

+106
-54
lines changed

cmd/gokapi/Main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import (
3434

3535
// versionGokapi is the current version in readable form.
3636
// Other version numbers can be modified in /build/go-generate/updateVersionNumbers.go
37-
const versionGokapi = "2.0.0-beta1"
37+
const versionGokapi = "2.0.0-beta2"
3838

3939
// The following calls update the version numbers, update documentation, minify Js/CSS and build the WASM modules
4040
//go:generate go run "../../build/go-generate/updateVersionNumbers.go"

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+
UploadDate: time.Now().Unix(),
224225
SizeBytes: 3 * 1024,
225226
DownloadsRemaining: 2,
226227
DownloadCount: 5,

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

Lines changed: 7 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 = 8
23+
const DatabaseSchemeVersion = 9
2424

2525
// New returns an instance
2626
func New(dbConfig models.DbConnection) (DatabaseProvider, error) {
@@ -90,6 +90,11 @@ func (p DatabaseProvider) Upgrade(currentDbVersion int) {
9090
}
9191
}
9292
}
93+
// < v2.0.0-beta3
94+
if currentDbVersion < 9 {
95+
err := p.rawSqlite(`ALTER TABLE "FileMetaData" ADD COLUMN UploadDate INTEGER NOT NULL DEFAULT 0;`)
96+
helper.Check(err)
97+
}
9398
}
9499

95100
func getLegacyE2EConfig(p DatabaseProvider) models.E2EInfoEncrypted {
@@ -217,6 +222,7 @@ func (p DatabaseProvider) createNewDatabase() error {
217222
"UnlimitedDownloads" INTEGER NOT NULL,
218223
"UnlimitedTime" INTEGER NOT NULL,
219224
"UserId" INTEGER NOT NULL,
225+
"UploadDate" INTEGER NOT NULL,
220226
PRIMARY KEY("Id")
221227
);
222228
CREATE TABLE "Hotlinks" (

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type schemaMetaData struct {
2727
UnlimitedDownloads int
2828
UnlimitedTime int
2929
UserId int
30+
UploadDate int64
3031
}
3132

3233
func (rowData schemaMetaData) ToFileModel() (models.File, error) {
@@ -48,6 +49,7 @@ func (rowData schemaMetaData) ToFileModel() (models.File, error) {
4849
UnlimitedDownloads: rowData.UnlimitedDownloads == 1,
4950
UnlimitedTime: rowData.UnlimitedTime == 1,
5051
UserId: rowData.UserId,
52+
UploadDate: rowData.UploadDate,
5153
}
5254

5355
buf := bytes.NewBuffer(rowData.Encryption)
@@ -67,7 +69,7 @@ func (p DatabaseProvider) GetAllMetadata() map[string]models.File {
6769
err = rows.Scan(&rowData.Id, &rowData.Name, &rowData.Size, &rowData.SHA1, &rowData.ExpireAt, &rowData.SizeBytes,
6870
&rowData.ExpireAtString, &rowData.DownloadsRemaining, &rowData.DownloadCount, &rowData.PasswordHash,
6971
&rowData.HotlinkId, &rowData.ContentType, &rowData.AwsBucket, &rowData.Encryption,
70-
&rowData.UnlimitedDownloads, &rowData.UnlimitedTime, &rowData.UserId)
72+
&rowData.UnlimitedDownloads, &rowData.UnlimitedTime, &rowData.UserId, &rowData.UploadDate)
7173
helper.Check(err)
7274
var metaData models.File
7375
metaData, err = rowData.ToFileModel()
@@ -101,7 +103,7 @@ func (p DatabaseProvider) GetMetaDataById(id string) (models.File, bool) {
101103
err := row.Scan(&rowData.Id, &rowData.Name, &rowData.Size, &rowData.SHA1, &rowData.ExpireAt, &rowData.SizeBytes,
102104
&rowData.ExpireAtString, &rowData.DownloadsRemaining, &rowData.DownloadCount, &rowData.PasswordHash,
103105
&rowData.HotlinkId, &rowData.ContentType, &rowData.AwsBucket, &rowData.Encryption,
104-
&rowData.UnlimitedDownloads, &rowData.UnlimitedTime, &rowData.UserId)
106+
&rowData.UnlimitedDownloads, &rowData.UnlimitedTime, &rowData.UserId, &rowData.UploadDate)
105107
if err != nil {
106108
if errors.Is(err, sql.ErrNoRows) {
107109
return result, false
@@ -131,6 +133,7 @@ func (p DatabaseProvider) SaveMetaData(file models.File) {
131133
ContentType: file.ContentType,
132134
AwsBucket: file.AwsBucket,
133135
UserId: file.UserId,
136+
UploadDate: file.UploadDate,
134137
}
135138

136139
if file.UnlimitedDownloads {
@@ -148,10 +151,10 @@ func (p DatabaseProvider) SaveMetaData(file models.File) {
148151

149152
_, err = p.sqliteDb.Exec(`INSERT OR REPLACE INTO FileMetaData (Id, Name, Size, SHA1, ExpireAt, SizeBytes, ExpireAtString,
150153
DownloadsRemaining, DownloadCount, PasswordHash, HotlinkId, ContentType, AwsBucket, Encryption,
151-
UnlimitedDownloads, UnlimitedTime, UserId) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
154+
UnlimitedDownloads, UnlimitedTime, UserId, UploadDate) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
152155
newData.Id, newData.Name, newData.Size, newData.SHA1, newData.ExpireAt, newData.SizeBytes, newData.ExpireAtString,
153156
newData.DownloadsRemaining, newData.DownloadCount, newData.PasswordHash, newData.HotlinkId, newData.ContentType,
154-
newData.AwsBucket, newData.Encryption, newData.UnlimitedDownloads, newData.UnlimitedTime, newData.UserId)
157+
newData.AwsBucket, newData.Encryption, newData.UnlimitedDownloads, newData.UnlimitedTime, newData.UserId, newData.UploadDate)
155158
helper.Check(err)
156159
}
157160

internal/models/FileList.go

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ type File struct {
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
21-
ExpireAt int64 `json:"ExpireAt" redis:"ExpireAt"` // "UTC timestamp of file expiry
21+
ExpireAt int64 `json:"ExpireAt" redis:"ExpireAt"` // UTC timestamp of file expiry
2222
SizeBytes int64 `json:"SizeBytes" redis:"SizeBytes"` // Filesize in bytes
23+
UploadDate int64 `json:"UploadDate" redis:"UploadDate"` // UTC timestamp of upload time
2324
DownloadsRemaining int `json:"DownloadsRemaining" redis:"DownloadsRemaining"` // The remaining downloads for this file
2425
DownloadCount int `json:"DownloadCount" redis:"DownloadCount"` // The amount of times the file has been downloaded
2526
UserId int `json:"UserId" redis:"UserId"` // The user ID of the uploader
@@ -31,26 +32,27 @@ type File struct {
3132

3233
// FileApiOutput will be displayed for public outputs from the ID, hiding sensitive information
3334
type FileApiOutput struct {
34-
Id string `json:"Id"` // The internal ID of the file
35-
Name string `json:"Name"` // The filename. Will be 'Encrypted file' for end-to-end encrypted files
36-
Size string `json:"Size"` // Filesize in a human-readable format
37-
HotlinkId string `json:"HotlinkId"` // If file is a picture file and can be hotlinked, this is the ID for the hotlink
38-
ContentType string `json:"ContentType"` // The MIME type for the file
39-
ExpireAtString string `json:"ExpireAtString"` // Time expiry in a human-readable format in local time
40-
UrlDownload string `json:"UrlDownload"` // The public download URL for the file
41-
UrlHotlink string `json:"UrlHotlink"` // The public hotlink URL for the file
42-
ExpireAt int64 `json:"ExpireAt"` // "UTC timestamp of file expiry
43-
SizeBytes int64 `json:"SizeBytes"` // Filesize in bytes
44-
DownloadsRemaining int `json:"DownloadsRemaining"` // The remaining downloads for this file
45-
DownloadCount int `json:"DownloadCount"` // The amount of times the file has been downloaded
46-
UnlimitedDownloads bool `json:"UnlimitedDownloads"` // True if the uploader did not limit the downloads
47-
UnlimitedTime bool `json:"UnlimitedTime"` // True if the uploader did not limit the time
48-
RequiresClientSideDecryption bool `json:"RequiresClientSideDecryption"` // True if the file has to be decrypted client-side
49-
IsEncrypted bool `json:"IsEncrypted"` // True if the file is encrypted
50-
IsEndToEndEncrypted bool `json:"IsEndToEndEncrypted"` // True if the file is end-to-end encrypted
51-
IsPasswordProtected bool `json:"IsPasswordProtected"` // True if a password has to be entered before downloading the file
52-
IsSavedOnLocalStorage bool `json:"IsSavedOnLocalStorage"` // True if the file does not use cloud storage
53-
UploaderId int `json:"UploaderId"` // The user ID of the uploader
35+
Id string `json:"Id"` // The internal ID of the file
36+
Name string `json:"Name"` // The filename. Will be 'Encrypted file' for end-to-end encrypted files
37+
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+
ContentType string `json:"ContentType"` // The MIME type for the file
40+
ExpireAtString string `json:"ExpireAtString"` // Time expiry in a human-readable format in local time
41+
UrlDownload string `json:"UrlDownload"` // The public download URL for the file
42+
UrlHotlink string `json:"UrlHotlink"` // The public hotlink URL for the file
43+
UploadDate int64 `json:"UploadDate" redis:"UploadDate"` // UTC timestamp of upload time
44+
ExpireAt int64 `json:"ExpireAt"` // "UTC timestamp of file expiry
45+
SizeBytes int64 `json:"SizeBytes"` // Filesize in bytes
46+
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+
UnlimitedDownloads bool `json:"UnlimitedDownloads"` // True if the uploader did not limit the downloads
49+
UnlimitedTime bool `json:"UnlimitedTime"` // True if the uploader did not limit the time
50+
RequiresClientSideDecryption bool `json:"RequiresClientSideDecryption"` // True if the file has to be decrypted client-side
51+
IsEncrypted bool `json:"IsEncrypted"` // True if the file is encrypted
52+
IsEndToEndEncrypted bool `json:"IsEndToEndEncrypted"` // True if the file is end-to-end encrypted
53+
IsPasswordProtected bool `json:"IsPasswordProtected"` // True if a password has to be entered before downloading the file
54+
IsSavedOnLocalStorage bool `json:"IsSavedOnLocalStorage"` // True if the file does not use cloud storage
55+
UploaderId int `json:"UploaderId"` // The user ID of the uploader
5456
}
5557

5658
// EncryptionInfo holds information about the encryption used on the file

internal/models/FileList_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@ func TestToJsonResult(t *testing.T) {
1313
Size: "10 B",
1414
SizeBytes: 10,
1515
SHA1: "sha256",
16-
ExpireAt: 50,
17-
ExpireAtString: "future",
16+
ExpireAt: 1750852108,
17+
ExpireAtString: "Wed Jun 25 2025 11:48:28",
1818
DownloadsRemaining: 1,
1919
PasswordHash: "pwhash",
2020
HotlinkId: "hotlinkid",
2121
ContentType: "text/html",
2222
AwsBucket: "test",
23+
UploadDate: 1748180908,
2324
UserId: 2,
2425
DownloadCount: 3,
2526
Encryption: EncryptionInfo{
@@ -30,8 +31,8 @@ func TestToJsonResult(t *testing.T) {
3031
UnlimitedDownloads: true,
3132
UnlimitedTime: true,
3233
}
33-
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,"UploaderId":2},"IncludeFilename":false}`)
34-
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,"UploaderId":2},"IncludeFilename":true}`)
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}`)
3536
}
3637

3738
func TestIsLocalStorage(t *testing.T) {

internal/models/FileUpload.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ type UploadRequest struct {
55
UserId int
66
AllowedDownloads int
77
Expiry int
8-
Password string
9-
ExternalUrl string
108
MaxMemory int
9+
ExpiryTimestamp int64
10+
RealSize int64
1111
UnlimitedDownload bool
1212
UnlimitedTime bool
1313
IsEndToEndEncrypted bool
14-
ExpiryTimestamp int64
15-
RealSize int64
14+
Password string
15+
ExternalUrl string
1616
}

internal/storage/FileServing.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ func createNewMetaData(hash string, fileHeader chunking.FileHeader, userId int,
301301
ContentType: fileHeader.ContentType,
302302
ExpireAt: uploadRequest.ExpiryTimestamp,
303303
ExpireAtString: FormatTimestamp(uploadRequest.ExpiryTimestamp),
304+
UploadDate: time.Now().Unix(),
304305
DownloadsRemaining: uploadRequest.AllowedDownloads,
305306
UnlimitedTime: uploadRequest.UnlimitedTime,
306307
UnlimitedDownloads: uploadRequest.UnlimitedDownload,

internal/storage/FileServing_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,9 @@ func TestNewFile(t *testing.T) {
228228
test.IsEqualBool(t, file.UnlimitedTime, true)
229229
test.IsEqualBool(t, file.UnlimitedDownloads, true)
230230

231+
withinLastSecond := file.UploadDate >= time.Now().Add(-1*time.Second).Unix() && file.UploadDate <= time.Now().Unix()
232+
test.IsEqualBool(t, withinLastSecond, true)
233+
231234
createBigFile("bigfile", 20)
232235
bigFile, _ := os.Open("bigfile")
233236
mimeHeader := make(textproto.MIMEHeader)
@@ -399,6 +402,8 @@ func TestNewFileFromChunk(t *testing.T) {
399402
retrievedFile, ok = database.GetMetaDataById(file.Id)
400403
test.IsEqualBool(t, ok, true)
401404
test.IsEqual(t, file, retrievedFile)
405+
withinLastSecond := file.UploadDate >= time.Now().Add(-1*time.Second).Unix() && file.UploadDate <= time.Now().Unix()
406+
test.IsEqualBool(t, withinLastSecond, true)
402407
err = os.Remove("test/data/6cca7a6905774e6d61a77dca3ad7a1f44581d6ab")
403408
test.IsNil(t, err)
404409

internal/webserver/Webserver.go

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -745,8 +745,8 @@ const (
745745
// Converts the globalConfig variable to an AdminView struct to pass the infos to
746746
// the admin template
747747
func (u *AdminView) convertGlobalConfig(view int, user models.User) *AdminView {
748-
var result []models.FileApiOutput
749-
var resultApi []models.ApiKey
748+
var metaDataList []models.FileApiOutput
749+
var apiKeyList []models.ApiKey
750750

751751
config := configuration.Get()
752752
u.IsInternalAuth = config.Authentication.Method == models.AuthenticationInternal
@@ -761,14 +761,9 @@ func (u *AdminView) convertGlobalConfig(view int, user models.User) *AdminView {
761761
}
762762
fileInfo, err := element.ToFileApiOutput(config.ServerUrl, config.IncludeFilename)
763763
helper.Check(err)
764-
result = append(result, fileInfo)
764+
metaDataList = append(metaDataList, fileInfo)
765765
}
766-
sort.Slice(result[:], func(i, j int) bool {
767-
if result[i].ExpireAt == result[j].ExpireAt {
768-
return result[i].Id > result[j].Id
769-
}
770-
return result[i].ExpireAt > result[j].ExpireAt
771-
})
766+
metaDataList = sortMetaData(metaDataList)
772767
case ViewAPI:
773768
for _, apiKey := range database.GetAllApiKeys() {
774769
// Double-checking if user of API key exists
@@ -780,16 +775,11 @@ func (u *AdminView) convertGlobalConfig(view int, user models.User) *AdminView {
780775
}
781776
if !apiKey.IsSystemKey {
782777
if apiKey.UserId == user.Id || user.HasPermissionManageApi() {
783-
resultApi = append(resultApi, apiKey)
778+
apiKeyList = append(apiKeyList, apiKey)
784779
}
785780
}
786781
}
787-
sort.Slice(resultApi[:], func(i, j int) bool {
788-
if resultApi[i].LastUsed == resultApi[j].LastUsed {
789-
return resultApi[i].Id < resultApi[j].Id
790-
}
791-
return resultApi[i].LastUsed > resultApi[j].LastUsed
792-
})
782+
apiKeyList = sortApiKeys(apiKeyList)
793783
case ViewLogs:
794784
u.Logs, _ = logging.GetAll()
795785
case ViewUsers:
@@ -809,9 +799,9 @@ func (u *AdminView) convertGlobalConfig(view int, user models.User) *AdminView {
809799
}
810800

811801
u.ServerUrl = config.ServerUrl
812-
u.Items = result
802+
u.Items = metaDataList
813803
u.PublicName = config.PublicName
814-
u.ApiKeys = resultApi
804+
u.ApiKeys = apiKeyList
815805
u.TimeNow = time.Now().Unix()
816806
u.IsAdminView = true
817807
u.ActiveView = view
@@ -826,6 +816,33 @@ func (u *AdminView) convertGlobalConfig(view int, user models.User) *AdminView {
826816
return u
827817
}
828818

819+
// sortMetaData arranges the provided array so that Fies are sorted by most recent upload first and if that is equal
820+
// then by most time remaining first. If that is equal, then sort by ID.
821+
func sortMetaData(input []models.FileApiOutput) []models.FileApiOutput {
822+
sort.Slice(input[:], func(i, j int) bool {
823+
if input[i].UploadDate != input[j].UploadDate {
824+
return input[i].UploadDate > input[j].UploadDate
825+
}
826+
if input[i].ExpireAt != input[j].ExpireAt {
827+
return input[i].ExpireAt > input[j].ExpireAt
828+
}
829+
return input[i].Id > input[j].Id
830+
})
831+
return input
832+
}
833+
834+
// sortApiKeys arranges the provided array so that API keys are sorted by most recent usage first and if that is equal
835+
// then by ID
836+
func sortApiKeys(input []models.ApiKey) []models.ApiKey {
837+
sort.Slice(input[:], func(i, j int) bool {
838+
if input[i].LastUsed != input[j].LastUsed {
839+
return input[i].LastUsed > input[j].LastUsed
840+
}
841+
return input[i].Id < input[j].Id
842+
})
843+
return input
844+
}
845+
829846
type userInfo struct {
830847
UploadCount int
831848
User models.User

0 commit comments

Comments
 (0)