Add directory uploader

This commit is contained in:
2023-05-27 15:50:44 +02:00
parent 82ebf18cdb
commit 71538e5d7a
20 changed files with 565 additions and 150 deletions

View File

@@ -0,0 +1,78 @@
<script>
import { createEventDispatcher } from "svelte";
import { fade } from "svelte/transition";
let dispatch = createEventDispatcher()
let dragging = false
const paste = (e) => {
if (e.clipboardData.files.length !== 0) {
e.preventDefault();
e.stopPropagation();
dispatch("upload", e.clipboardData.files)
}
}
const drop = async e => {
dragging = false;
if (e.dataTransfer.files || e.dataTransfer.items) {
e.stopPropagation();
e.preventDefault();
}
// if directory support is available
if(e.dataTransfer && e.dataTransfer.items && e.dataTransfer.items.length > 0) {
for (let i = 0; i < e.dataTransfer.items.length; i++) {
let entry = await e.dataTransfer.items[i].webkitGetAsEntry();
if (entry) {
await read_dir_recursive(entry);
}
}
} else if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length > 0) {
dispatch("upload_files", e.dataTransfer.files)
}
}
const read_dir_recursive = item => {
if (item.isDirectory) {
item.createReader().readEntries(entries => {
entries.forEach(entry => {
read_dir_recursive(entry);
});
});
} else {
item.file(file => {
dispatch("upload_file", file)
});
}
}
</script>
<svelte:window
on:dragover|preventDefault|stopPropagation={() => { dragging = true }}
on:dragenter|preventDefault|stopPropagation={() => { dragging = true }}
on:dragleave|preventDefault|stopPropagation={() => { dragging = false }}
on:drop={drop}
on:paste={paste}
/>
{#if dragging}
<div class="drag_target" transition:fade={{duration: 200}}>
Drop files here to upload them
</div>
{/if}
<style>
.drag_target {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 50px;
font-size: 2em;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 100px;
box-shadow: 0 0 10px 10px rgba(0, 0, 0, 0.5);
}
</style>

View File

@@ -0,0 +1,106 @@
// Uploads a file to the logged in user's pixeldrain account. If no user is
// logged in the file is uploaded anonymously.
//
// on_progress reports progress on the file upload, parameter 1 is the uploaded
// file size and parameter 2 is the total file size
//
// on_success is called when the upload is done, the only parameter is the file
// ID
//
// on_error is called when the upload has failed. The parameters are the error
import { fs_get_node, fs_mkdirall } from "../FilesystemAPI"
import { fs_path_url, fs_split_path } from "../FilesystemUtil"
// code and an error message
export const upload_file = async (file, bucket, path, on_progress, on_success, on_error) => {
// Check the file size limit. For free accounts it's 20 GB
if (window.user.subscription.file_size_limit === 0) {
window.user.subscription.file_size_limit = 20e9
}
if (file.size > window.user.subscription.file_size_limit) {
on_failure(
"file_too_large",
"This file is too large. Check out the Pro subscription to increase the file size limit"
)
return
}
// Check if the parent directory exists
try {
await ensure_parent_dir(bucket, path)
} catch (err) {
if (err.value && err.message) {
on_error(err.value, err.message)
} else {
on_error(err, err)
}
return
}
let xhr = new XMLHttpRequest();
xhr.open("PUT", fs_path_url(bucket, path), true);
xhr.timeout = 86400000; // 24 hours, to account for slow connections
xhr.upload.addEventListener("progress", evt => {
if (on_progress && evt.lengthComputable) {
on_progress(evt.loaded, evt.total)
}
});
xhr.onreadystatechange = () => {
// readystate 4 means the upload is done
if (xhr.readyState !== 4) {
return
}
if (xhr.status >= 100 && xhr.status < 400) {
// Request is a success
on_success(JSON.parse(xhr.response).id)
} else if (xhr.status >= 400) {
// Request failed
console.log("Upload error. status: " + xhr.status + " response: " + xhr.response);
let resp;
if (xhr.status === 429) {
resp = {
value: "too_many_requests",
message: "Too many requests. Please wait a few seconds",
}
} else {
resp = JSON.parse(xhr.response)
}
on_error(resp.value, resp.message)
} else if (xhr.status === 0) {
on_error("request_failed", "Your request did not arrive, check your network connection")
} else {
on_error(xhr.responseText, xhr.responseText)
}
};
xhr.send(file);
}
const ensure_parent_dir = async (bucket, path) => {
let parent = fs_split_path(path).parent
console.debug("Checking if parent directory exists", parent)
try {
let node = await fs_get_node(bucket, parent)
if (node.path[node.base_index].type !== "dir") {
throw "Path " + path + " is not a directory"
}
} catch (err) {
if (err.value && err.value === "path_not_found") {
// Directory does not exist. Create it
await fs_mkdirall(bucket, parent)
console.debug("Created parent directory", parent)
} else {
throw err
}
}
}

View File

@@ -0,0 +1,68 @@
<script>
import { createEventDispatcher } from "svelte";
import { fade } from "svelte/transition";
import { upload_file } from "./UploadFunc";
import ProgressBar from "../../util/ProgressBar.svelte";
let dispatch = createEventDispatcher()
export let job = {
file: null,
name: "",
id: "",
status: "",
}
export let total = 0
export let loaded = 0
let error_code = ""
let error_message = ""
export const start = () => {
upload_file(
job.file,
job.bucket,
job.path,
(prog_loaded, prog_total) => {
loaded = prog_loaded
total = prog_total
},
async () => {
job.status = "finished"
dispatch("finished")
},
(code, message) => {
console.log("error", code, message)
error_code = code
error_message = message
job.status = "error"
// Wait with reporting so the user can read the error message
setTimeout(() => dispatch("finished"), 60000)
},
)
job.status = "uploading"
}
</script>
<div class="upload_progress" transition:fade={{duration: 200}} class:error={job.status === "error"}>
{job.file.name}<br/>
{#if error_code !== ""}
{error_message}<br/>
{error_code}<br/>
{/if}
<ProgressBar total={total} used={loaded}/>
</div>
<style>
.upload_progress {
display: block;
padding: 2px 4px 1px 4px;
margin: 4px;
border-radius: 4px;
}
.error {
background: var(--danger_color);
color: var(--highlight_text_color);
}
</style>

View File

@@ -0,0 +1,202 @@
<script>
import { createEventDispatcher, tick } from "svelte";
import { fade } from "svelte/transition";
import DropUpload from "./DropUpload.svelte";
import UploadProgress from "./UploadProgress.svelte";
let dispatch = createEventDispatcher()
export let fs_state
let file_input_field;
let file_input_change = e => {
// Start uploading the files async
upload_files(e.target.files)
// This resets the file input field
file_input_field.nodeValue = ""
}
export const pick_files = () => {
file_input_field.click()
}
export let drop_upload = false
let visible = false
let upload_queue = [];
let task_id_counter = 0
export const upload_files = async (files) => {
if (files.length === 0) {
return
}
if (current_dir.type !== "dir") {
alert("Can only upload to directory")
return
}
// Add files to the queue
for (let i = 0; i < files.length; i++) {
await upload_file(files[i])
}
}
export const upload_file = async file => {
if (fs_state.base.type !== "dir") {
alert("Can only upload to directory")
return
}
if (file.type === "" && file.size === 0) {
return
}
upload_queue.push({
task_id: task_id_counter,
file: file,
bucket: fs_state.root.id,
path: fs_state.base.path + "/" + file.webkitRelativePath,
component: null,
status: "queued",
total_size: file.size,
loaded_size: 0,
})
task_id_counter++
// Reassign array and wait for tick to complete. After the tick is completed
// each upload progress bar will have bound itself to its array item
upload_queue = upload_queue
if (active_uploads === 0 && state !== "uploading") {
state = "uploading"
visible = true
await tick()
await start_upload()
}
}
let active_uploads = 0
let state = "idle"
const start_upload = async () => {
for (let i = 0; i < upload_queue.length && active_uploads < 3; i++) {
if (upload_queue[i]) {
if (upload_queue[i].status === "queued") {
active_uploads++
upload_queue[i].component.start()
upload_queue[i].status = "uploading"
}
}
}
if (active_uploads === 0) {
state = "finished"
let file_ids = []
upload_queue.forEach(job => {
if (job.status === "finished" && job.id !== "") {
file_ids.push(job.id)
}
})
dispatch("uploads_finished", file_ids)
upload_queue = []
setTimeout(() => {
if (state === "finished") {
visible = false
}
}, 10000)
} else {
state = "uploading"
}
}
const finish_upload = (e) => {
active_uploads--
upload_queue = upload_queue
start_upload()
}
const leave_confirmation = e => {
if (state === "uploading") {
e.preventDefault()
e.returnValue = "If you close this page your files will stop uploading. Do you want to continue?"
return e.returnValue
} else {
return null
}
}
</script>
<svelte:window on:beforeunload={leave_confirmation} />
<input
bind:this={file_input_field}
on:change={file_input_change}
class="upload_input" type="file" name="file" multiple="multiple"
/>
{#if visible}
<div class="upload_widget" transition:fade={{duration: 200}}>
<div class="header">
{#if state === "idle"}
Waiting for files
{:else if state === "uploading"}
Uploading files...
{:else if state === "finished"}
Done
{/if}
</div>
<div class="body">
{#each upload_queue as job}
{#if job.status !== "finished"}
<UploadProgress bind:this={job.component} job={job} on:finished={finish_upload}/>
{/if}
{/each}
</div>
</div>
{/if}
{#if drop_upload}
<DropUpload on:upload_files={e => upload_files(e.detail)} on:upload_file={e => upload_file(e.detail)}/>
{/if}
<style>
.upload_input {
visibility: hidden;
position: fixed;
width: 0;
height: 0;
}
.upload_widget {
position: absolute;
display: flex;
flex-direction: column;
width: 500px;
max-width: 80%;
height: auto;
max-height: 50%;
right: 20px;
bottom: 20px;
border-radius: 20px 20px 8px 8px;
overflow: hidden;
box-shadow: 1px 1px 8px var(--shadow_color);
}
.header {
flex: 0 0 auto;
background: var(--background_color);
color: var(--background_text_color);
text-align: center;
font-size: 1.2em;
padding: 4px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.body {
flex: 1 1 auto;
background: var(--body_color);
color: var(--body_text_color);
overflow-y: auto;
text-align: left;
}
</style>