Added option to disable upload and time limit during upload #13

This commit is contained in:
Marc Ole Bulling
2022-02-15 20:20:53 +01:00
parent a2daccd972
commit f42faf8f53
18 changed files with 227 additions and 87 deletions
@@ -60,7 +60,12 @@ func updateConfig(settings *models.Configuration, env *environment.Environment)
// < v1.5.0
if settings.ConfigVersion < 11 {
legacyConfig := loadLegacyConfigPreDb(env)
datastorage.SaveUploadDefaults(legacyConfig.DefaultDownloads, legacyConfig.DefaultExpiry, legacyConfig.DefaultPassword)
uploadValues := models.LastUploadValues{
Downloads: legacyConfig.DefaultDownloads,
TimeExpiry: legacyConfig.DefaultExpiry,
Password: legacyConfig.DefaultPassword,
}
datastorage.SaveUploadDefaults(uploadValues)
for _, hotlink := range legacyConfig.Hotlinks {
datastorage.SaveHotlink(models.File{Id: hotlink.FileId, HotlinkId: hotlink.Id})
@@ -17,9 +17,7 @@ const prefixApiKey = "apikey:id:"
const prefixFile = "file:id:"
const prefixHotlink = "hotlink:id:"
const prefixSessions = "session:id:"
const idDefaultDownloads = "default:downloads"
const idDefaultExpiry = "default:expiry"
const idDefaultPassword = "default:password"
const idLastUploadConfig = "default:lastupload"
const maxKeySize = 96
@@ -39,7 +37,7 @@ func Init(dbPath string) {
// GetLengthAvailable returns the maximum length for a key name
func GetLengthAvailable() int {
maxLength := 0
for _, key := range []string{prefixApiKey, prefixFile, prefixHotlink, prefixSessions, idDefaultDownloads, idDefaultExpiry, idDefaultPassword} {
for _, key := range []string{prefixApiKey, prefixFile, prefixHotlink, prefixSessions, idLastUploadConfig} {
length := len(key)
if length > maxLength {
maxLength = length
@@ -242,37 +240,34 @@ func SaveSession(id string, session models.Session, expiry time.Duration) {
// GetUploadDefaults returns the last used setting for amount of downloads allowed, last expiry in days and
// a password for the file
func GetUploadDefaults() (int, int, string) {
downloads := 1
expiry := 14
password := ""
if database.Has([]byte(idDefaultDownloads)) {
bufByte, err := database.Get([]byte(idDefaultDownloads))
helper.Check(err)
downloads = byteToInt(bufByte)
func GetUploadDefaults() models.LastUploadValues {
defaultValues := models.LastUploadValues{
Downloads: 1,
TimeExpiry: 14,
Password: "",
UnlimitedDownload: false,
UnlimitedTime: false,
}
if database.Has([]byte(idDefaultExpiry)) {
bufByte, err := database.Get([]byte(idDefaultExpiry))
result := models.LastUploadValues{}
if database.Has([]byte(idLastUploadConfig)) {
value, err := database.Get([]byte(idLastUploadConfig))
helper.Check(err)
expiry = byteToInt(bufByte)
}
if database.Has([]byte(idDefaultPassword)) {
buf, err := database.Get([]byte(idDefaultPassword))
buf := bytes.NewBuffer(value)
dec := gob.NewDecoder(buf)
err = dec.Decode(&result)
helper.Check(err)
password = string(buf)
return result
}
return downloads, expiry, password
return defaultValues
}
// SaveUploadDefaults saves the last used setting for an upload
func SaveUploadDefaults(downloads, expiry int, password string) {
err := database.Put([]byte(idDefaultDownloads), intToByte(downloads))
func SaveUploadDefaults(values models.LastUploadValues) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
err := enc.Encode(values)
helper.Check(err)
err = database.Put([]byte(idDefaultExpiry), intToByte(expiry))
helper.Check(err)
err = database.Put([]byte(idDefaultPassword), []byte(password))
helper.Check(err)
err = database.Sync()
err = database.Put([]byte(idLastUploadConfig), buf.Bytes())
helper.Check(err)
}
@@ -149,16 +149,26 @@ func TestSession(t *testing.T) {
}
func TestUploadDefaults(t *testing.T) {
downloads, expiry, password := GetUploadDefaults()
test.IsEqualInt(t, downloads, 1)
test.IsEqualInt(t, expiry, 14)
test.IsEqualString(t, password, "")
defaults := GetUploadDefaults()
test.IsEqualInt(t, defaults.Downloads, 1)
test.IsEqualInt(t, defaults.TimeExpiry, 14)
test.IsEqualString(t, defaults.Password, "")
test.IsEqualBool(t, defaults.UnlimitedDownload, false)
test.IsEqualBool(t, defaults.UnlimitedTime, false)
SaveUploadDefaults(20, 30, "abcd")
downloads, expiry, password = GetUploadDefaults()
test.IsEqualInt(t, downloads, 20)
test.IsEqualInt(t, expiry, 30)
test.IsEqualString(t, password, "abcd")
SaveUploadDefaults(models.LastUploadValues{
Downloads: 20,
TimeExpiry: 30,
Password: "abcd",
UnlimitedDownload: true,
UnlimitedTime: true,
})
defaults = GetUploadDefaults()
test.IsEqualInt(t, defaults.Downloads, 20)
test.IsEqualInt(t, defaults.TimeExpiry, 30)
test.IsEqualString(t, defaults.Password, "abcd")
test.IsEqualBool(t, defaults.UnlimitedDownload, true)
test.IsEqualBool(t, defaults.UnlimitedTime, true)
}
func TestBinaryConversion(t *testing.T) {
+1 -1
View File
@@ -46,7 +46,7 @@ func TestEnvLoad(t *testing.T) {
test.IsEqualInt(t, env.LengthId, 5)
os.Setenv("GOKAPI_LENGTH_ID", "80")
env = New()
test.IsEqualInt(t, env.LengthId, 79)
test.IsEqualInt(t, env.LengthId, 78)
os.Unsetenv("GOKAPI_LENGTH_ID")
env = New()
os.Setenv("GOKAPI_LENGTH_ID", "15")
+8
View File
@@ -19,6 +19,14 @@ type Configuration struct {
MaxFileSizeMB int `json:"MaxFileSizeMB"`
}
type LastUploadValues struct {
Downloads int
TimeExpiry int
Password string
UnlimitedDownload bool
UnlimitedTime bool
}
// ToJson returns an idented JSon representation
func (c Configuration) ToJson() []byte {
result, err := json.MarshalIndent(c, "", " ")
+2
View File
@@ -18,6 +18,8 @@ type File struct {
HotlinkId string `json:"HotlinkId"`
ContentType string `json:"ContentType"`
AwsBucket string `json:"AwsBucket"`
UnlimitedDownloads bool `json:"UnlimitedDownloads"`
UnlimitedTime bool `json:"UnlimitedTime"`
}
// ToJsonResult converts the file info to a json String used for returning a result for an upload
+1 -1
View File
@@ -21,5 +21,5 @@ func TestToJsonResult(t *testing.T) {
HotlinkId: "hotlinkid",
ContentType: "text/html",
}
test.IsEqualString(t, file.ToJsonResult("serverurl/"), `{"Result":"OK","FileInfo":{"Id":"testId","Name":"testName","Size":"10 B","SHA256":"sha256","ExpireAt":50,"ExpireAtString":"future","DownloadsRemaining":1,"PasswordHash":"pwhash","HotlinkId":"hotlinkid","ContentType":"text/html","AwsBucket":""},"Url":"serverurl/d?id=","HotlinkUrl":"serverurl/hotlink/"}`)
test.IsEqualString(t, file.ToJsonResult("serverurl/"), `{"Result":"OK","FileInfo":{"Id":"testId","Name":"testName","Size":"10 B","SHA256":"sha256","ExpireAt":50,"ExpireAtString":"future","DownloadsRemaining":1,"PasswordHash":"pwhash","HotlinkId":"hotlinkid","ContentType":"text/html","AwsBucket":"","UnlimitedDownloads":false,"UnlimitedTime":false},"Url":"serverurl/d?id=","HotlinkUrl":"serverurl/hotlink/"}`)
}
+9 -7
View File
@@ -2,11 +2,13 @@ package models
// UploadRequest is used to set an upload request
type UploadRequest struct {
AllowedDownloads int
Expiry int
ExpiryTimestamp int64
Password string
ExternalUrl string
MaxMemory int
DataDir string
AllowedDownloads int
Expiry int
ExpiryTimestamp int64
Password string
ExternalUrl string
MaxMemory int
DataDir string
UnlimitedDownload bool
UnlimitedTime bool
}
+16 -2
View File
@@ -49,6 +49,8 @@ func NewFile(fileContent io.Reader, fileHeader *multipart.FileHeader, uploadRequ
DownloadsRemaining: uploadRequest.AllowedDownloads,
PasswordHash: configuration.HashPassword(uploadRequest.Password, true),
ContentType: fileHeader.Header.Get("Content-Type"),
UnlimitedTime: uploadRequest.UnlimitedTime,
UnlimitedDownloads: uploadRequest.UnlimitedDownload,
}
addHotlink(&file)
if aws.IsAvailable() {
@@ -143,7 +145,7 @@ func GetFile(id string) (models.File, bool) {
if !ok {
return emptyResult, false
}
if file.ExpireAt < time.Now().Unix() || file.DownloadsRemaining < 1 {
if isExpiredFile(file, time.Now().Unix()) {
return emptyResult, false
}
if !FileExists(file, configuration.Get().DataDir) {
@@ -227,7 +229,7 @@ func CleanUp(periodic bool) {
wasItemDeleted := false
for key, element := range datastorage.GetAllMetadata() {
fileExists := FileExists(element, configuration.Get().DataDir)
if (element.ExpireAt < timeNow || element.DownloadsRemaining < 1 || !fileExists) && !downloadstatus.IsCurrentlyDownloading(element) {
if !fileExists || isExpiredFileWithoutDownload(element, timeNow) {
deleteFile := true
for _, secondLoopElement := range datastorage.GetAllMetadata() {
if element.Id != secondLoopElement.Id && element.SHA256 == secondLoopElement.SHA256 {
@@ -253,6 +255,18 @@ func CleanUp(periodic bool) {
}
}
func isExpiredFile(file models.File, timeNow int64) bool {
return (file.ExpireAt < timeNow && !file.UnlimitedTime) ||
(file.DownloadsRemaining < 1 && !file.UnlimitedDownloads)
}
func isExpiredFileWithoutDownload(file models.File, timeNow int64) bool {
if downloadstatus.IsCurrentlyDownloading(file) {
return false
}
return isExpiredFile(file, timeNow)
}
func deleteSource(file models.File, dataDir string) {
var err error
if file.AwsBucket != "" {
+20
View File
@@ -112,8 +112,24 @@ func TestNewFile(t *testing.T) {
test.IsEqualInt(t, retrievedFile.DownloadsRemaining, 1)
test.IsEqualInt(t, len(retrievedFile.Id), 20)
test.IsEqualInt(t, int(retrievedFile.ExpireAt), 2147483600)
test.IsEqualBool(t, file.UnlimitedTime, false)
test.IsEqualBool(t, file.UnlimitedDownloads, false)
idNewFile = file.Id
request.UnlimitedDownload = true
file, err = NewFile(bytes.NewReader(content), &header, request)
test.IsEqualBool(t, file.UnlimitedTime, false)
test.IsEqualBool(t, file.UnlimitedDownloads, true)
request.UnlimitedDownload = false
request.UnlimitedTime = true
file, err = NewFile(bytes.NewReader(content), &header, request)
test.IsEqualBool(t, file.UnlimitedTime, true)
test.IsEqualBool(t, file.UnlimitedDownloads, false)
request.UnlimitedDownload = true
file, err = NewFile(bytes.NewReader(content), &header, request)
test.IsEqualBool(t, file.UnlimitedTime, true)
test.IsEqualBool(t, file.UnlimitedDownloads, true)
createBigFile("bigfile", 20)
bigFile, _ := os.Open("bigfile")
mimeHeader = make(textproto.MIMEHeader)
@@ -249,6 +265,8 @@ func TestCleanUp(t *testing.T) {
test.IsEqualString(t, files["wefffewhtrhhtrhtrhtr"].Name, "smallfile3")
test.IsEqualString(t, files["n1tSTAGj8zan9KaT4u6p"].Name, "picture.jpg")
test.IsEqualString(t, files["deletedfile123456789"].Name, "DeletedFile")
test.IsEqualString(t, files["unlimitedDownload"].Name, "unlimitedDownload")
test.IsEqualString(t, files["unlimitedTime"].Name, "unlimitedTime")
test.FileExists(t, "test/data/2341354656543213246465465465432456898794")
CleanUp(false)
@@ -259,6 +277,8 @@ func TestCleanUp(t *testing.T) {
test.IsEqualString(t, files["Wzol7LyY2QVczXynJtVo"].Name, "smallfile2")
test.IsEqualString(t, files["e4TjE7CokWK0giiLNxDL"].Name, "smallfile2")
test.IsEqualString(t, files["wefffewhtrhhtrhtrhtr"].Name, "smallfile3")
test.IsEqualString(t, files["unlimitedDownload"].Name, "unlimitedDownload")
test.IsEqualString(t, files["unlimitedTime"].Name, "unlimitedTime")
test.IsEqualString(t, files["n1tSTAGj8zan9KaT4u6p"].Name, "picture.jpg")
file, _ := GetFile("n1tSTAGj8zan9KaT4u6p")
@@ -34,7 +34,13 @@ func Create(initFiles bool) {
os.WriteFile(configFile, configTestFile, 0777)
datastorage.Init("./test/filestorage.db")
writeTestSessions()
datastorage.SaveUploadDefaults(3, 20, "123")
datastorage.SaveUploadDefaults(models.LastUploadValues{
Downloads: 3,
TimeExpiry: 20,
Password: "123",
UnlimitedDownload: false,
UnlimitedTime: false,
})
writeTestFiles()
datastorage.SaveHotlink(models.File{Id: "n1tSTAGj8zan9KaT4u6p", HotlinkId: "PhSs6mFtf8O5YGlLMfNw9rYXx9XRNkzCnJZpQBi7inunv3Z4A.jpg", ExpireAt: time.Now().Add(time.Hour).Unix()})
writeApiKeyys()
@@ -46,6 +52,7 @@ func Create(initFiles bool) {
os.WriteFile("test/data/c4f9375f9834b4e7f0a528cc65c055702bf5f24a", []byte("456"), 0777)
os.WriteFile("test/data/e017693e4a04a59d0b0f400fe98177fe7ee13cf7", []byte("789"), 0777)
os.WriteFile("test/data/2341354656543213246465465465432456898794", []byte("abc"), 0777)
os.WriteFile("test/data/unlimtedtest", []byte("def"), 0777)
os.WriteFile("test/fileupload.jpg", []byte("abc"), 0777)
}
}
@@ -270,6 +277,28 @@ func writeTestFiles() {
ContentType: "application/octet-stream",
AwsBucket: "gokapi-test",
})
datastorage.SaveMetaData(models.File{
Id: "unlimitedDownload",
Name: "unlimitedDownload",
Size: "8 B",
SHA256: "unlimtedtest",
ExpireAt: 2147483646,
ExpireAtString: "2021-05-04 15:19",
DownloadsRemaining: 0,
ContentType: "text/html",
UnlimitedDownloads: true,
})
datastorage.SaveMetaData(models.File{
Id: "unlimitedTime",
Name: "unlimitedTime",
Size: "8 B",
SHA256: "unlimtedtest",
ExpireAt: 0,
ExpireAtString: "2021-05-04 15:19",
DownloadsRemaining: 1,
ContentType: "text/html",
UnlimitedTime: true,
})
}
var configTestFile = []byte(`{
+21 -14
View File
@@ -326,19 +326,21 @@ type DownloadView struct {
// UploadView contains parameters for the admin menu template
type UploadView struct {
Items []models.File
ApiKeys []models.ApiKey
Url string
HotlinkUrl string
TimeNow int64
DefaultDownloads int
DefaultExpiry int
DefaultPassword string
IsAdminView bool
IsMainView bool
IsApiView bool
MaxFileSize int
IsLogoutAvailable bool
Items []models.File
ApiKeys []models.ApiKey
Url string
HotlinkUrl string
TimeNow int64
IsAdminView bool
IsMainView bool
IsApiView bool
MaxFileSize int
IsLogoutAvailable bool
DefaultDownloads int
DefaultExpiry int
DefaultPassword string
DefaultUnlimitedDownload bool
DefaultUnlimitedTime bool
}
// Converts the globalConfig variable to an UploadView struct to pass the infos to
@@ -381,7 +383,12 @@ func (u *UploadView) convertGlobalConfig(isMainView bool) *UploadView {
u.IsMainView = isMainView
u.MaxFileSize = configuration.Get().MaxFileSizeMB
u.IsLogoutAvailable = authentication.IsLogoutAvailable()
u.DefaultDownloads, u.DefaultExpiry, u.DefaultPassword = datastorage.GetUploadDefaults()
defaultValues := datastorage.GetUploadDefaults()
u.DefaultDownloads = defaultValues.Downloads
u.DefaultExpiry = defaultValues.TimeExpiry
u.DefaultPassword = defaultValues.Password
u.DefaultUnlimitedDownload = defaultValues.UnlimitedDownload
u.DefaultUnlimitedTime = defaultValues.UnlimitedTime
return u
}
+25 -12
View File
@@ -47,26 +47,39 @@ func parseConfig(values formOrHeader, setNewDefaults bool) models.UploadRequest
password := values.Get("password")
allowedDownloadsInt, err := strconv.Atoi(allowedDownloads)
if err != nil {
previous, _, _ := datastorage.GetUploadDefaults()
allowedDownloadsInt = previous
previousValues := datastorage.GetUploadDefaults()
allowedDownloadsInt = previousValues.Downloads
}
expiryDaysInt, err := strconv.Atoi(expiryDays)
if err != nil {
_, previous, _ := datastorage.GetUploadDefaults()
expiryDaysInt = previous
previousValues := datastorage.GetUploadDefaults()
expiryDaysInt = previousValues.TimeExpiry
}
unlimitedDownload := values.Get("isUnlimitedDownload") == "true"
unlimitedTime := values.Get("isUnlimitedTime") == "true"
if setNewDefaults {
datastorage.SaveUploadDefaults(allowedDownloadsInt, expiryDaysInt, password)
values := models.LastUploadValues{
Downloads: allowedDownloadsInt,
TimeExpiry: expiryDaysInt,
Password: password,
UnlimitedDownload: unlimitedDownload,
UnlimitedTime: unlimitedTime,
}
datastorage.SaveUploadDefaults(values)
}
settings := configuration.Get()
return models.UploadRequest{
AllowedDownloads: allowedDownloadsInt,
Expiry: expiryDaysInt,
ExpiryTimestamp: time.Now().Add(time.Duration(expiryDaysInt) * time.Hour * 24).Unix(),
Password: password,
ExternalUrl: settings.ServerUrl,
MaxMemory: settings.MaxMemory,
DataDir: settings.DataDir,
AllowedDownloads: allowedDownloadsInt,
Expiry: expiryDaysInt,
ExpiryTimestamp: time.Now().Add(time.Duration(expiryDaysInt) * time.Hour * 24).Unix(),
Password: password,
ExternalUrl: settings.ServerUrl,
MaxMemory: settings.MaxMemory,
DataDir: settings.DataDir,
UnlimitedTime: unlimitedTime,
UnlimitedDownload: unlimitedDownload,
}
}
@@ -35,16 +35,16 @@ func TestParseConfig(t *testing.T) {
password: "123",
}
config := parseConfig(data, false)
downloads, _, _ := datastorage.GetUploadDefaults()
defaults := datastorage.GetUploadDefaults()
test.IsEqualInt(t, config.AllowedDownloads, 9)
test.IsEqualString(t, config.Password, "123")
test.IsEqualInt(t, config.Expiry, 5)
test.IsEqualInt(t, downloads, 3)
test.IsEqualInt(t, defaults.Downloads, 3)
config = parseConfig(data, true)
downloads, _, _ = datastorage.GetUploadDefaults()
test.IsEqualInt(t, downloads, 9)
datastorage.SaveUploadDefaults(3, 20, "")
defaults = datastorage.GetUploadDefaults()
test.IsEqualInt(t, defaults.Downloads, 9)
datastorage.SaveUploadDefaults(models.LastUploadValues{Downloads: 3, TimeExpiry: 20})
data.allowedDownloads = ""
data.expiryDays = "invalid"
config = parseConfig(data, false)
@@ -65,6 +65,10 @@ a:hover {
text-align: center;
}
.form-control:disabled {
background: #bababa;
}
.break {
flex-basis: 100%;
height: 0;
+22 -2
View File
@@ -18,6 +18,8 @@ Dropzone.options.uploaddropzone = {
formData.append("allowedDownloads", document.getElementById("allowedDownloads").value);
formData.append("expiryDays", document.getElementById("expiryDays").value);
formData.append("password", document.getElementById("password").value);
formData.append("isUnlimitedDownload", !document.getElementById("enableDownloadLimit").checked);
formData.append("isUnlimitedTime", !document.getElementById("enableTimeLimit").checked);
});
},
};
@@ -33,6 +35,16 @@ document.onpaste = function(event){
}
function checkBoxChanged(checkBox, correspondingInput) {
let disable = !checkBox.checked;
if (disable) {
document.getElementById(correspondingInput).setAttribute("disabled", "");
} else {
document.getElementById(correspondingInput).removeAttribute("disabled");
}
}
function parseData(data) {
if (!data) return {"Result":"error"};
if (typeof data === 'object') return data;
@@ -64,8 +76,16 @@ function addRow(jsonText) {
}
cell1.innerText = item.Name;
cell2.innerText = item.Size;
cell3.innerText = item.DownloadsRemaining;
cell4.innerText = item.ExpireAtString;
if (item.UnlimitedDownloads) {
cell3.innerText = "Unlimited";
} else {
cell3.innerText = item.DownloadsRemaining;
}
if (item.UnlimitedTime) {
cell4.innerText = "Unlimited";
} else {
cell4.innerText = item.ExpireAtString;
}
cell5.innerHTML = '<a target="_blank" style="color: inherit" href="'+jsonObject.Url+item.Id+'">'+jsonObject.Url+item.Id+'</a>'+lockIcon;
let buttons = "<button type=\"button\" data-clipboard-text=\""+jsonObject.Url+item.Id+"\" class=\"copyurl btn btn-outline-light btn-sm\">Copy URL</button> ";
@@ -14,18 +14,21 @@
<div class="container-md">
<div class="row">
<div class="form-group col">
<input class="form-check-input" type="checkbox" name="enableDownloadLimit" id="enableDownloadLimit" onchange="checkBoxChanged(this, 'allowedDownloads')" value="" aria-label="Enable Download Limit" {{ if not .DefaultUnlimitedDownload }} checked {{ end }}>
<label class="control-label small" for="allowedDownloads">Allowed downloads</label>
<input type="number" class="form-control admin-input" value="{{ .DefaultDownloads }}" name="allowedDownloads" id="allowedDownloads" min="1"/>
<input type="number" class="form-control admin-input" value="{{ .DefaultDownloads }}" name="allowedDownloads" id="allowedDownloads" min="1" {{ if .DefaultUnlimitedDownload }} disabled {{ end }}/>
</div>
<div class="break"></div>
<div class="form-group col">
<input class="form-check-input" type="checkbox" name="enableTimeLimit" id="enableTimeLimit" onchange="checkBoxChanged(this, 'expiryDays')" value="" aria-label="Enable Time Limit" {{ if not .DefaultUnlimitedTime }} checked {{ end }}>
<label class="control-label small" for="expiryDays">Expiry in days</label>
<input type="number" class="form-control admin-input" value="{{ .DefaultExpiry }}" name="expiryDays" id="expiryDays" min="1"/>
<input type="number" class="form-control admin-input" value="{{ .DefaultExpiry }}" name="expiryDays" id="expiryDays" min="1" {{ if .DefaultUnlimitedTime }} disabled {{ end }}/>
</div>
<div class="break"></div>
<div class="form-group col">
<input class="form-check-input" type="checkbox" name="enablePassword" id="enablePassword" onchange="checkBoxChanged(this, 'password')" value="" aria-label="Enable Password Protection" {{ if ne .DefaultPassword "" }} checked {{ end }}>
<label class="control-label small" for="password">Password</label>
<input class="form-control admin-input" value="{{ .DefaultPassword }}" name="password" id="password" placeholder="None"/>
<input class="form-control admin-input" value="{{ .DefaultPassword }}" name="password" id="password" placeholder="None" {{ if eq .DefaultPassword "" }} disabled {{ end }}/>
</div>
</div>
</div>
@@ -49,8 +52,16 @@
<tr>
<td scope="col">{{ .Name }}</td>
<td scope="col">{{ .Size }}</td>
{{ if .UnlimitedDownloads }}
<td scope="col">Unlimited</td>
{{ else }}
<td scope="col">{{ .DownloadsRemaining }}</td>
{{ end }}
{{ if .UnlimitedTime }}
<td scope="col">Unlimited</td>
{{ else }}
<td scope="col">{{ .ExpireAtString }}</td>
{{ end }}
<td scope="col"><a target="_blank" href="{{ $.Url }}{{ .Id }}">{{ $.Url }}{{ .Id }}</a>{{ if ne .PasswordHash "" }} &#128274;{{ end }}</td>
<td scope="col"><button type="button" data-clipboard-text="{{ $.Url }}{{ .Id }}" class="copyurl btn btn-outline-light btn-sm">Copy URL</button>
{{ if ne .HotlinkId "" }}
@@ -70,7 +81,7 @@
</div>
</div>
<script src="./js/admin.js?v=6"></script>
<script src="./js/admin.js?v=7"></script>
<script>
Dropzone.options.uploaddropzone["maxFilesize"] = {{ .MaxFileSize }};
</script>
@@ -11,7 +11,7 @@
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link href="css/cover.css?v=3" rel="stylesheet">
<link href="css/cover.css?v=4" rel="stylesheet">
{{ if .IsAdminView }}
<title>{{template "app_name"}} Admin</title>
<link href="./assets/dist/css/dropzone.min.css" rel="stylesheet">