Skip to content

Commit 3fad4c2

Browse files
committed
First working prototype
1 parent cc59fff commit 3fad4c2

File tree

7 files changed

+181
-20
lines changed

7 files changed

+181
-20
lines changed

internal/models/FileList.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ func (f *File) ToFileApiOutput(serverUrl string, useFilenameInUrl bool) (FileApi
9797
if !f.IsFileRequest() {
9898
result.UrlHotlink = getHotlinkUrl(result, serverUrl, useFilenameInUrl)
9999
result.UrlDownload = getDownloadUrl(result, serverUrl, useFilenameInUrl)
100+
result.UploaderId = f.UserId
100101
}
101-
result.UploaderId = f.UserId
102102
result.IsPendingDeletion = f.IsPendingForDeletion()
103103
result.FileRequestId = f.UploadRequestId
104104
result.ExpireAtString = time.Unix(f.ExpireAt, 0).UTC().Format("2006-01-02 15:04:05")

internal/storage/FileServing.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -291,25 +291,26 @@ func encryptChunkFile(file *os.File, metadata *models.File) (*os.File, error) {
291291
return tempFileEnc, nil
292292
}
293293

294-
func createNewMetaData(hash string, fileHeader chunking.FileHeader, userId int, uploadRequest models.UploadParameters) models.File {
294+
func createNewMetaData(hash string, fileHeader chunking.FileHeader, userId int, params models.UploadParameters) models.File {
295295
file := models.File{
296296
Id: createNewId(),
297297
Name: fileHeader.Filename,
298298
SHA1: hash,
299299
Size: helper.ByteCountSI(fileHeader.Size),
300300
SizeBytes: fileHeader.Size,
301301
ContentType: fileHeader.ContentType,
302-
ExpireAt: uploadRequest.ExpiryTimestamp,
302+
ExpireAt: params.ExpiryTimestamp,
303303
UploadDate: time.Now().Unix(),
304-
DownloadsRemaining: uploadRequest.AllowedDownloads,
305-
UnlimitedTime: uploadRequest.UnlimitedTime,
306-
UnlimitedDownloads: uploadRequest.UnlimitedDownload,
307-
PasswordHash: configuration.HashPassword(uploadRequest.Password, true),
304+
DownloadsRemaining: params.AllowedDownloads,
305+
UnlimitedTime: params.UnlimitedTime,
306+
UnlimitedDownloads: params.UnlimitedDownload,
307+
PasswordHash: configuration.HashPassword(params.Password, true),
308308
UserId: userId,
309+
UploadRequestId: params.FileRequestId,
309310
}
310-
if uploadRequest.IsEndToEndEncrypted {
311+
if params.IsEndToEndEncrypted {
311312
file.Encryption = models.EncryptionInfo{IsEndToEndEncrypted: true, IsEncrypted: true}
312-
file.Size = helper.ByteCountSI(uploadRequest.RealSize)
313+
file.Size = helper.ByteCountSI(params.RealSize)
313314
}
314315
if isEncryptionRequested() {
315316
file.Encryption.IsEncrypted = true

internal/webserver/Webserver.go

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Handling of webserver and requests / uploads
77
import (
88
"bytes"
99
"context"
10+
"crypto/subtle"
1011
"embed"
1112
"encoding/base64"
1213
"errors"
@@ -115,6 +116,7 @@ func Start() {
115116
mux.HandleFunc("/login", showLogin)
116117
mux.HandleFunc("/logs", requireLogin(showLogs, true, false))
117118
mux.HandleFunc("/logout", doLogout)
119+
mux.HandleFunc("/publicUpload", showPublicUpload)
118120
mux.HandleFunc("/uploadChunk", requireLogin(uploadChunk, false, false))
119121
mux.HandleFunc("/uploadStatus", requireLogin(sse.GetStatusSSE, false, false))
120122
mux.HandleFunc("/users", requireLogin(showUserAdmin, true, false))
@@ -357,9 +359,12 @@ func validateNewPassword(newPassword string, user models.User) (string, string,
357359

358360
// Handling of /error
359361
func showError(w http.ResponseWriter, r *http.Request) {
360-
const invalidFile = 0
361-
const noCipherSupplied = 1
362-
const wrongCipher = 2
362+
const (
363+
invalidFile = iota
364+
noCipherSupplied
365+
wrongCipher
366+
invalidFileRequest
367+
)
363368

364369
errorReason := invalidFile
365370
if r.URL.Query().Has("e2e") {
@@ -368,6 +373,9 @@ func showError(w http.ResponseWriter, r *http.Request) {
368373
if r.URL.Query().Has("key") {
369374
errorReason = wrongCipher
370375
}
376+
if r.URL.Query().Has("fr") {
377+
errorReason = invalidFileRequest
378+
}
371379
err := templateFolder.ExecuteTemplate(w, "error", genericView{
372380
ErrorId: errorReason,
373381
PublicName: configuration.Get().PublicName,
@@ -523,7 +531,7 @@ type LoginView struct {
523531
// If it exists, a download form is shown, or a password needs to be entered.
524532
func showDownload(w http.ResponseWriter, r *http.Request) {
525533
addNoCacheHeader(w)
526-
keyId := queryUrl(w, r, "error")
534+
keyId := queryUrl(w, r, "id", "error")
527535
file, ok := storage.GetFile(keyId)
528536
if !ok || file.IsFileRequest() {
529537
redirect(w, "error")
@@ -597,8 +605,8 @@ func showHotlink(w http.ResponseWriter, r *http.Request) {
597605

598606
// Checks if a file is associated with the GET parameter from the current URL
599607
// Stops for 500ms to limit brute forcing if invalid key and redirects to redirectUrl
600-
func queryUrl(w http.ResponseWriter, r *http.Request, redirectUrl string) string {
601-
keys, ok := r.URL.Query()["id"]
608+
func queryUrl(w http.ResponseWriter, r *http.Request, keyword string, redirectUrl string) string {
609+
keys, ok := r.URL.Query()[keyword]
602610
if !ok || len(keys[0]) < configuration.Get().LengthId {
603611
select {
604612
case <-time.After(500 * time.Millisecond):
@@ -883,6 +891,35 @@ type userInfo struct {
883891
User models.User
884892
}
885893

894+
// Handling of /publicUpload
895+
func showPublicUpload(w http.ResponseWriter, r *http.Request) {
896+
addNoCacheHeader(w)
897+
fileRequestId := queryUrl(w, r, "id", "error?fr")
898+
request, ok := filerequest.Get(fileRequestId)
899+
if !ok {
900+
redirect(w, "error?fr")
901+
return
902+
}
903+
apiKey := queryUrl(w, r, "key", "error?fr")
904+
if subtle.ConstantTimeCompare([]byte(request.ApiKey), []byte(apiKey)) != 1 {
905+
redirect(w, "error?fr")
906+
return
907+
}
908+
909+
config := configuration.Get()
910+
911+
view := filerequestView{
912+
PublicName: config.PublicName,
913+
ApiKey: apiKey,
914+
FileRequestId: fileRequestId,
915+
ChunkSize: configuration.Get().ChunkSize,
916+
CustomContent: customStaticInfo,
917+
}
918+
919+
err := templateFolder.ExecuteTemplate(w, "publicUpload", view)
920+
helper.CheckIgnoreTimeout(err)
921+
}
922+
886923
// Handling of /uploadChunk
887924
// If the user is authenticated, this parses the uploaded chunk and stores it
888925
func uploadChunk(w http.ResponseWriter, r *http.Request) {
@@ -917,7 +954,7 @@ func downloadFileWithNameInUrl(w http.ResponseWriter, r *http.Request) {
917954
// Handling of /downloadFile
918955
// Outputs the file to the user and reduces the download remaining count for the file
919956
func downloadFile(w http.ResponseWriter, r *http.Request) {
920-
id := queryUrl(w, r, "error")
957+
id := queryUrl(w, r, "id", "error")
921958
serveFile(id, true, w, r)
922959
}
923960

@@ -1065,3 +1102,14 @@ type oauthErrorView struct {
10651102
ErrorProvidedMessage string
10661103
CustomContent customStatic
10671104
}
1105+
1106+
// A view containing parameters for a generic template
1107+
type filerequestView struct {
1108+
IsAdminView bool
1109+
IsDownloadView bool
1110+
PublicName string
1111+
ApiKey string
1112+
FileRequestId string
1113+
ChunkSize int
1114+
CustomContent customStatic
1115+
}

internal/webserver/api/routing.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ var routes = []apiRoute{
9292
Url: "/chunk/uploadRequestComplete",
9393
ApiPerm: models.ApiPermNone,
9494
IsFileRequestApi: true,
95-
execution: apiChunkAdd,
95+
execution: apiChunkUploadRequestComplete,
9696
RequestParser: &paramChunkUploadRequestComplete{},
9797
},
9898
{

internal/webserver/web/templates/html_error.tmpl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
{{ if eq .ErrorId 2 }}
1717
This file is encrypted and an incorrect key has been passed.<br><br>If this file is end-to-end encrypted, please contact the uploader to give you the correct link, including the value after the hash.
1818
{{ end }}
19+
{{ if eq .ErrorId 3 }}
20+
Unable to upload files.<br><br>Either the request has expired or an invalid request was submitted.
21+
{{ end }}
1922
<br>&nbsp;
2023
</p>
2124
</div>
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
{{define "publicUpload"}}{{template "header" .}}
2+
3+
<input type="file" id="fileInput" multiple />
4+
<br><br>
5+
<button onclick="startUpload()">Upload</button>
6+
7+
<div id="status"></div>
8+
9+
<script>
10+
const CHUNK_SIZE = {{ .ChunkSize }} * 1024 * 1024;
11+
const UPLOAD_URL = "./api/chunk/uploadRequestAdd";
12+
const COMPLETE_URL = "./api/chunk/uploadRequestComplete";
13+
14+
const FILE_REQUEST_ID = "{{ .FileRequestId }}";
15+
const API_KEY = "{{ .ApiKey }}";
16+
17+
function generateUUID() {
18+
return crypto.randomUUID();
19+
}
20+
21+
async function startUpload() {
22+
const files = document.getElementById("fileInput").files;
23+
const status = document.getElementById("status");
24+
status.innerHTML = "";
25+
26+
for (const file of files) {
27+
const fileDiv = document.createElement("div");
28+
fileDiv.className = "file";
29+
fileDiv.innerHTML = `
30+
<strong>${file.name}</strong><br>
31+
<progress value="0" max="${file.size}"></progress>
32+
<span class="progressText">0%</span>
33+
`;
34+
status.appendChild(fileDiv);
35+
36+
const progressBar = fileDiv.querySelector("progress");
37+
const progressText = fileDiv.querySelector(".progressText");
38+
39+
const uuid = generateUUID();
40+
let offset = 0;
41+
42+
while (offset < file.size) {
43+
const chunk = file.slice(offset, offset + CHUNK_SIZE);
44+
45+
const formData = new FormData();
46+
formData.append("file", chunk);
47+
formData.append("uuid", uuid);
48+
formData.append("filesize", file.size);
49+
formData.append("offset", offset);
50+
51+
const response = await fetch(UPLOAD_URL, {
52+
method: "POST",
53+
body: formData,
54+
headers: {
55+
'apikey': API_KEY,
56+
"fileRequestId": FILE_REQUEST_ID
57+
}
58+
});
59+
60+
if (!response.ok) {
61+
throw new Error(`Chunk upload failed: ${response.status}`);
62+
}
63+
64+
offset += chunk.size;
65+
progressBar.value = offset;
66+
progressText.textContent = Math.floor((offset / file.size) * 100) + "%";
67+
}
68+
69+
// Finalize upload
70+
await finalizeUpload(file, uuid);
71+
progressText.textContent = "✔ Completed";
72+
}
73+
}
74+
75+
async function finalizeUpload(file, uuid) {
76+
const headers = {
77+
"uuid": uuid,
78+
"fileRequestId": FILE_REQUEST_ID,
79+
"filename": encodeFilename(file.name),
80+
"filesize": file.size,
81+
'apikey': API_KEY,
82+
"contenttype": file.type || "application/octet-stream"
83+
};
84+
85+
const response = await fetch(COMPLETE_URL, {
86+
method: "POST",
87+
headers: headers
88+
});
89+
90+
if (!response.ok) {
91+
throw new Error(`Finalize failed: ${response.status}`);
92+
}
93+
94+
return response;
95+
}
96+
97+
function encodeFilename(name) {
98+
return "base64:" + btoa(unescape(encodeURIComponent(name)));
99+
}
100+
</script>
101+
102+
{{ template "pagename" "PublicUpload"}}
103+
{{ template "customjs" .}}
104+
105+
{{template "footer"}}
106+
{{end}}

internal/webserver/web/templates/html_uploadrequest.tmpl

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<h3 class="card-title mb-0">File Requests</h3>
1212
</div>
1313
<div class="col text-end">
14-
<button id="button-newapi" class="btn btn-outline-light" onclick="newFileRequest()">
14+
<button id="button-newfr" class="btn btn-outline-light" onclick="newFileRequest()">
1515
<i class="bi bi-plus-circle-fill"></i>
1616
</button>
1717
</div>
@@ -38,7 +38,7 @@
3838

3939
{{ range .FileRequests }}
4040
<tr id="row-{{ .Id }}" class="no-bottom-border">
41-
<td>{{ .Name }}</td>
41+
<td><a href="{{ $.ServerUrl }}publicUpload?id={{ .Id }}&key={{ .ApiKey }}" target="_blank">{{ .Name }}</a></td>
4242
{{ template "uRFileCell" . }}
4343
<td>{{ .GetReadableTotalSize }}</td>
4444
<td><span id="cell-lastupdate-{{ .Id }}"></span></td>
@@ -53,6 +53,9 @@
5353
<div class="btn-group" role="group">
5454
{{ template "uRDownloadbutton" . }}
5555

56+
57+
<button id="copy-{{ .Id }}" type="button" data-clipboard-text="{{ $.ServerUrl }}publicUpload?id={{ .Id }}&key={{ .ApiKey }}" class="copyurl btn btn-outline-light btn-sm" onclick="showToast(1000);" title="Copy URL"><i class="bi bi-copy"></i></button>
58+
5659
<button id="edit-{{ .Id }}" type="button" title="Edit request" class="btn btn-outline-light btn-sm" onclick="editFileRequest('{{ .Id }}', '{{ .Name }}', {{ .MaxFiles }}, {{ .MaxSize }}, {{ .Expiry }})">
5760
<i class="bi bi-pencil"></i></button>
5861

@@ -100,7 +103,7 @@
100103
</div>
101104
</div>
102105
</div>
103-
<div id="toastnotification" class="toastnotification" data-default="API key copied to clipboard">Toast Text</div>
106+
<div id="toastnotification" class="toastnotification" data-default="URL copied to clipboard">Toast Text</div>
104107
</div>
105108
</div>
106109
<script src="./js/min/admin.min.{{ template "js_admin_version"}}.js"></script>

0 commit comments

Comments
 (0)