Skip to content

Commit

Permalink
feat: integrate tus.io for resumable and chunked uploads (#2145)
Browse files Browse the repository at this point in the history
  • Loading branch information
TobiasGoerke committed Jul 28, 2023
1 parent 2744f7d commit 7b35815
Show file tree
Hide file tree
Showing 24 changed files with 694 additions and 66 deletions.
10 changes: 7 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,9 +334,13 @@ func quickSetup(flags *pflag.FlagSet, d pythonData) {
},
AuthMethod: "",
Branding: settings.Branding{},
Commands: nil,
Shell: nil,
Rules: nil,
Tus: settings.Tus{
ChunkSize: settings.DefaultTusChunkSize,
RetryCount: settings.DefaultTusRetryCount,
},
Commands: nil,
Shell: nil,
Rules: nil,
}

var err error
Expand Down
2 changes: 2 additions & 0 deletions files/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
"github.com/filebrowser/filebrowser/v2/rules"
)

const PERM = 0664

// FileInfo describes a file.
type FileInfo struct {
*Listing
Expand Down
247 changes: 218 additions & 29 deletions frontend/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"noty": "^3.2.0-beta",
"pretty-bytes": "^6.0.0",
"qrcode.vue": "^1.7.0",
"tus-js-client": "^3.1.0",
"utif": "^3.1.0",
"vue": "^2.6.10",
"vue-async-computed": "^3.9.0",
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/api/files.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createURL, fetchURL, removePrefix } from "./utils";
import { baseURL } from "@/utils/constants";
import store from "@/store";
import { upload as postTus, useTus } from "./tus";

export async function fetch(url) {
url = removePrefix(url);
Expand Down Expand Up @@ -78,6 +79,22 @@ export function download(format, ...files) {
}

export async function post(url, content = "", overwrite = false, onupload) {
// Use the pre-existing API if:
const useResourcesApi =
// a folder is being created
url.endsWith("/") ||
// We're not using http(s)
(content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol)) ||
// Tus is disabled / not applicable
!(await useTus(content));

return useResourcesApi
? postResources(url, content, overwrite, onupload)
: postTus(url, content, overwrite, onupload);
}

async function postResources(url, content = "", overwrite = false, onupload) {
url = removePrefix(url);

let bufferContent;
Expand Down
85 changes: 85 additions & 0 deletions frontend/src/api/tus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import * as tus from "tus-js-client";
import { tusEndpoint, tusSettings } from "@/utils/constants";
import store from "@/store";
import { removePrefix } from "@/api/utils";
import { fetchURL } from "./utils";

const RETRY_BASE_DELAY = 1000;
const RETRY_MAX_DELAY = 20000;

export async function upload(url, content = "", overwrite = false, onupload) {
if (!tusSettings) {
// Shouldn't happen as we check for tus support before calling this function
throw new Error("Tus.io settings are not defined");
}

url = removePrefix(url);
let resourceUrl = `${tusEndpoint}${url}?override=${overwrite}`;

await createUpload(resourceUrl);

return new Promise((resolve, reject) => {
let upload = new tus.Upload(content, {
uploadUrl: resourceUrl,
chunkSize: tusSettings.chunkSize,
retryDelays: computeRetryDelays(tusSettings),
parallelUploads: 1,
storeFingerprintForResuming: false,
headers: {
"X-Auth": store.state.jwt,
},
onError: function (error) {
reject("Upload failed: " + error);
},
onProgress: function (bytesUploaded) {
// Emulate ProgressEvent.loaded which is used by calling functions
// loaded is specified in bytes (https://developer.mozilla.org/en-US/docs/Web/API/ProgressEvent/loaded)
if (typeof onupload === "function") {
onupload({ loaded: bytesUploaded });
}
},
onSuccess: function () {
resolve();
},
});
upload.start();
});
}

async function createUpload(resourceUrl) {
let headResp = await fetchURL(resourceUrl, {
method: "POST",
});
if (headResp.status !== 201) {
throw new Error(
`Failed to create an upload: ${headResp.status} ${headResp.statusText}`
);
}
}

function computeRetryDelays(tusSettings) {
if (!tusSettings.retryCount || tusSettings.retryCount < 1) {
// Disable retries altogether
return null;
}
// The tus client expects our retries as an array with computed backoffs
// E.g.: [0, 3000, 5000, 10000, 20000]
const retryDelays = [];
let delay = 0;

for (let i = 0; i < tusSettings.retryCount; i++) {
retryDelays.push(Math.min(delay, RETRY_MAX_DELAY));
delay =
delay === 0 ? RETRY_BASE_DELAY : Math.min(delay * 2, RETRY_MAX_DELAY);
}

return retryDelays;
}

export async function useTus(content) {
return isTusSupported() && content instanceof Blob;
}

function isTusSupported() {
return tus.isSupported === true;
}
10 changes: 9 additions & 1 deletion frontend/src/components/prompts/Replace.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
>
{{ $t("buttons.cancel") }}
</button>
<button
class="button button--flat button--blue"
@click="showAction"
:aria-label="$t('buttons.continue')"
:title="$t('buttons.continue')"
>
{{ $t("buttons.continue") }}
</button>
<button
class="button button--flat button--red"
@click="showConfirm"
Expand All @@ -34,6 +42,6 @@ import { mapState } from "vuex";
export default {
name: "replace",
computed: mapState(["showConfirm"]),
computed: mapState(["showConfirm", "showAction"]),
};
</script>
10 changes: 10 additions & 0 deletions frontend/src/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,16 @@ body.rtl .breadcrumbs .chevron {
}
}

/* * * * * * * * * * * * * * * *
* SETTINGS TUS *
* * * * * * * * * * * * * * * */

.tusConditionalSettings input:disabled {
background-color: #ddd;
color: #999;
cursor: not-allowed;
}

/* * * * * * * * * * * * * * * *
* SETTINGS RULES *
* * * * * * * * * * * * * * * */
Expand Down
9 changes: 7 additions & 2 deletions frontend/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"toggleSidebar": "Toggle sidebar",
"update": "Update",
"upload": "Upload",
"openFile": "Open file"
"openFile": "Open file",
"continue": "Continue"
},
"download": {
"downloadFile": "Download File",
Expand Down Expand Up @@ -148,7 +149,7 @@
"rename": "Rename",
"renameMessage": "Insert a new name for",
"replace": "Replace",
"replaceMessage": "One of the files you're trying to upload is conflicting because of its name. Do you wish to replace the existing one?\n",
"replaceMessage": "One of the files you're trying to upload is conflicting because of its name. Do you wish to continue to upload or replace the existing one?\n",
"schedule": "Schedule",
"scheduleMessage": "Pick a date and time to schedule the publication of this post.",
"show": "Show",
Expand Down Expand Up @@ -185,6 +186,10 @@
"commandRunnerHelp": "Here you can set commands that are executed in the named events. You must write one per line. The environment variables {0} and {1} will be available, being {0} relative to {1}. For more information about this feature and the available environment variables, please read the {2}.",
"commandsUpdated": "Commands updated!",
"createUserDir": "Auto create user home dir while adding new user",
"tusUploads": "Chunked Uploads",
"tusUploadsHelp": "File Browser supports chunked file uploads, allowing for the creation of efficient, reliable, resumable and chunked file uploads even on unreliable networks.",
"tusUploadsChunkSize": "Indicates to maximum size of a request (direct uploads will be used for smaller uploads). You may input a plain integer denoting a bytes input or a string like 10MB, 1GB etc.",
"tusUploadsRetryCount": "Number of retries to perform if a chunk fails to upload.",
"userHomeBasePath": "Base path for user home directories",
"userScopeGenerationPlaceholder": "The scope will be auto generated",
"createUserHomeDirectory": "Create user home directory",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const state = {
show: null,
showShell: false,
showConfirm: null,
showAction: null,
};

export default new Vuex.Store({
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/store/mutations.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const mutations = {
closeHovers: (state) => {
state.show = null;
state.showConfirm = null;
state.showAction = null;
},
toggleShell: (state) => {
state.showShell = !state.showShell;
Expand All @@ -17,6 +18,9 @@ const mutations = {

state.show = value.prompt;
state.showConfirm = value.confirm;
if (value.action !== undefined) {
state.showAction = value.action;
}
},
showError: (state) => {
state.show = "error";
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/utils/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ const theme = window.FileBrowser.Theme;
const enableThumbs = window.FileBrowser.EnableThumbs;
const resizePreview = window.FileBrowser.ResizePreview;
const enableExec = window.FileBrowser.EnableExec;
const tusSettings = window.FileBrowser.TusSettings;
const origin = window.location.origin;
const tusEndpoint = `${baseURL}/api/tus`;

export {
name,
Expand All @@ -34,5 +36,7 @@ export {
enableThumbs,
resizePreview,
enableExec,
tusSettings,
origin,
tusEndpoint,
};
10 changes: 10 additions & 0 deletions frontend/src/views/files/Listing.vue
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,11 @@ export default {
if (conflict) {
this.$store.commit("showHover", {
prompt: "replace",
action: (event) => {
event.preventDefault();
this.$store.commit("closeHovers");
upload.handleFiles(files, path, false);
},
confirm: (event) => {
event.preventDefault();
this.$store.commit("closeHovers");
Expand Down Expand Up @@ -731,6 +736,11 @@ export default {
if (conflict) {
this.$store.commit("showHover", {
prompt: "replace",
action: (event) => {
event.preventDefault();
this.$store.commit("closeHovers");
upload.handleFiles(files, path, false);
},
confirm: (event) => {
event.preventDefault();
this.$store.commit("closeHovers");
Expand Down

0 comments on commit 7b35815

Please sign in to comment.