diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 054b5bf..eef3bb1 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -3,6 +3,8 @@ package config import "strconv" const ( + KeyModtext = "modt" + KeyUploadMaxFileSizeBytes = "upload.max_file_size_bytes" KeyUploadMultiMaxFiles = "upload.multi.max_files" KeyUploadMaxHours = "upload.max_hours" @@ -18,6 +20,8 @@ const ( // Defaults (used when DB does not have an override) const ( + DefaultModt = "A_SERVICE_BY_BRAMMIE15" + DefaultUploadMaxFileSizeBytes int64 = 10 << 30 // 10 GiB (matches MaxMultipartMemory intent) DefaultUploadMultiMaxFiles = 50 DefaultUploadMaxHours = 24 * 7 // 7 days @@ -36,6 +40,8 @@ const ( // Code duplication be dammed func DefaultKeyValues() map[string]string { return map[string]string{ + KeyModtext: DefaultModt, + KeyUploadMaxFileSizeBytes: strconv.FormatInt(DefaultUploadMaxFileSizeBytes, 10), KeyUploadMultiMaxFiles: strconv.Itoa(DefaultUploadMultiMaxFiles), KeyUploadMaxHours: strconv.Itoa(DefaultUploadMaxHours), diff --git a/internal/web/config.go b/internal/web/config.go index fb2285a..da018c1 100644 --- a/internal/web/config.go +++ b/internal/web/config.go @@ -12,6 +12,8 @@ type ConfigPageData struct { Success bool Error string + MODT string + UploadMaxFileSizeMB int64 UploadMultiMaxFiles int UploadMaxHours int @@ -33,6 +35,7 @@ func (h *Handler) ConfigPage(c *gin.Context) { maxBytes := cfg.GetInt64Default(config.KeyUploadMaxFileSizeBytes, config.DefaultUploadMaxFileSizeBytes) data := ConfigPageData{ Success: c.Query("saved") == "1", + MODT: cfg.GetStringDefault(config.KeyModtext, config.DefaultModt), UploadMaxFileSizeMB: maxBytes / (1024 * 1024), UploadMultiMaxFiles: cfg.GetIntDefault(config.KeyUploadMultiMaxFiles, config.DefaultUploadMultiMaxFiles), UploadMaxHours: cfg.GetIntDefault(config.KeyUploadMaxHours, config.DefaultUploadMaxHours), @@ -82,6 +85,12 @@ func (h *Handler) ConfigSave(c *gin.Context) { return n, nil } + newMODT, err := strconv.Unquote(`"` + c.PostForm("site_modt") + `"`) + if err != nil { + h.renderConfigError(c, "invalid modtext") + return + } + maxMB, err := parseInt64("upload_max_file_size_mb", 1, 1024*1024) if err != nil { h.renderConfigError(c, "invalid max file size") @@ -142,6 +151,8 @@ func (h *Handler) ConfigSave(c *gin.Context) { return } + _ = cfg.SetString(config.KeyModtext, newMODT) + _ = cfg.SetString(config.KeyRateLimitLoginPerMinute, strconv.Itoa(loginPerMin)) _ = cfg.SetString(config.KeyRateLimitLoginBurst, strconv.Itoa(loginBurst)) _ = cfg.SetString(config.KeyRateLimitApiPerMinute, strconv.Itoa(apiPerMin)) diff --git a/internal/web/handler.go b/internal/web/handler.go index af3800c..7ba349d 100644 --- a/internal/web/handler.go +++ b/internal/web/handler.go @@ -2,6 +2,7 @@ package web import ( "ResendIt/internal/buildinfo" + "ResendIt/internal/config" "ResendIt/internal/file" "os" "strconv" @@ -33,6 +34,7 @@ func NewHandler(fileService *file.Service, cfg ConfigService) *Handler { func (h *Handler) Index(c *gin.Context) { c.HTML(200, "index.html", gin.H{ "title": "Home", + "MODT": h.configService.GetStringDefault(config.KeyModtext, config.DefaultModt), }) } @@ -50,7 +52,9 @@ func (h *Handler) FileView(c *gin.Context) { fileRecord, err := h.fileService.GetFileByViewID(id) if err != nil { - c.HTML(404, "error.html", nil) + c.HTML(404, "error.html", gin.H{ + "MODT": h.configService.GetStringDefault(config.KeyModtext, config.DefaultModt), + }) return } @@ -58,6 +62,7 @@ func (h *Handler) FileView(c *gin.Context) { deleteKey := fileRecord.DeletionID c.HTML(200, "complete.html", gin.H{ + "MODT": h.configService.GetStringDefault(config.KeyModtext, config.DefaultModt), "Filename": fileRecord.Filename, "DownloadID": downloadKey, "DeleteID": deleteKey, diff --git a/static/js/upload.js b/static/js/upload.js new file mode 100644 index 0000000..d7be781 --- /dev/null +++ b/static/js/upload.js @@ -0,0 +1,179 @@ +const zone = document.getElementById('drop-zone'); +const input = document.getElementById('fileInput'); +const uploadBtn = document.getElementById('uploadBtn'); +const cancelBtn = document.getElementById('cancelBtn'); + +const progressText = document.getElementById("progress-text"); +const statsText = document.getElementById("stats-text"); +const speedText = document.getElementById("speed-text"); +const etaText = document.getElementById("eta-text"); +const progressBar = document.getElementById("progress-bar"); +const progressContainer = document.getElementById("progress-container"); + +let currentXhr = null; + +function formatBytes(bytes, decimals = 2) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} + +function formatTime(seconds) { + if (!isFinite(seconds) || seconds < 0) return "--:--"; + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = Math.floor(seconds % 60); + return [ + h > 0 ? h : null, + (h > 0 ? m.toString().padStart(2, '0') : m), + s.toString().padStart(2, '0') + ].filter(x => x !== null).join(':'); +} + +zone.onclick = () => input.click(); + +zone.ondragover = (e) => { + e.preventDefault(); + zone.classList.add('active'); +}; +zone.ondragleave = () => zone.classList.remove('active'); + +zone.ondrop = (e) => { + e.preventDefault(); + zone.classList.remove('active'); + if (e.dataTransfer.files.length) { + input.files = e.dataTransfer.files; + input.dispatchEvent(new Event('change')); + } +}; + +input.onchange = () => { + if (input.files.length) { + showFiles(input.files); + uploadBtn.disabled = false; + } else { + uploadBtn.disabled = true; + } +}; + +function showFiles(fileList) { + const files = Array.from(fileList || []); + if (files.length === 0) return; + const total = files.reduce((acc, f) => acc + f.size, 0); + if (files.length === 1) { + document.getElementById('dz-text').innerText = + `${files[0].name} [${formatBytes(files[0].size)}]`; + } else { + document.getElementById('dz-text').innerText = + `${files.length} FILES [${formatBytes(total)}] — will be zipped`; + } +} + +uploadBtn.onclick = () => { + if (!input.files.length) return; + if (input.files.length === 1) { + handleUploadSingle(input.files[0]); + } else { + handleUploadMulti(input.files); + } +}; + +cancelBtn.onclick = (e) => { + e.stopPropagation(); + if (currentXhr) { + currentXhr.abort(); + alert("Upload cancelled."); + location.reload(); + } +}; + +function commonFormData() { + const fd = new FormData(); + fd.append("once", document.getElementById("once").checked ? "true" : "false"); + const hours = parseInt(document.getElementById("duration").value, 10); + fd.append("duration", hours); + return fd; +} + +function startUploadUI() { + uploadBtn.disabled = true; + uploadBtn.innerText = "UPLOADING..."; + cancelBtn.classList.remove('hidden'); + progressContainer.classList.remove("hidden"); + progressText.classList.remove("hidden"); + statsText.classList.remove("hidden"); +} + +function setupXHRHandlers(xhr) { + let startTime = Date.now(); + + xhr.upload.onprogress = (e) => { + if (e.lengthComputable) { + const percent = Math.round((e.loaded / e.total) * 100); + progressBar.style.width = percent + "%"; + progressText.innerText = percent + "%"; + + const elapsedSeconds = (Date.now() - startTime) / 1000; + if (elapsedSeconds > 0) { + const bytesPerSecond = e.loaded / elapsedSeconds; + const remainingBytes = e.total - e.loaded; + const secondsRemaining = remainingBytes / bytesPerSecond; + speedText.innerText = formatBytes(bytesPerSecond) + "/S"; + etaText.innerText = formatTime(secondsRemaining); + } + } + }; + + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300) { + try { + const data = JSON.parse(xhr.responseText); + if (data.error) throw new Error(data.error); + window.location.href = "/f/" + data.view_key; + } catch (err) { + console.error("Invalid response:", xhr.responseText); + alert("Server error"); + } + } else { + alert("Upload failed"); + } + }; + + xhr.onerror = () => { + if (xhr.statusText !== "abort") { + alert("Upload failed"); + location.reload(); + } + }; +} + +function handleUploadSingle(file) { + startUploadUI(); + const fd = commonFormData(); + fd.append("file", file); + const xhr = new XMLHttpRequest(); + currentXhr = xhr; + setupXHRHandlers(xhr); + xhr.open("POST", "/api/files/upload"); + xhr.send(fd); +} + +function handleUploadMulti(fileList) { + startUploadUI(); + const fd = commonFormData(); + Array.from(fileList).forEach(f => fd.append("files", f)); + const xhr = new XMLHttpRequest(); + currentXhr = xhr; + setupXHRHandlers(xhr); + xhr.open("POST", "/api/files/upload-multi"); + xhr.send(fd); +} + +function copy(id) { + const el = document.getElementById(id); + el.select(); + document.execCommand('copy'); +} \ No newline at end of file diff --git a/templates/complete.html b/templates/complete.html index 560f451..6c7f897 100644 --- a/templates/complete.html +++ b/templates/complete.html @@ -6,6 +6,7 @@ Send.it - File Ready +