Implentment large paste upload (frontend)

Signed-off-by: Joe Ma <rikkaneko23@gmail.com>
This commit is contained in:
Joe Ma 2024-02-04 19:25:33 +08:00
parent 2f67469ef5
commit 72c965787f
No known key found for this signature in database
GPG key ID: 7A0ECF5F5EDC587F
2 changed files with 105 additions and 41 deletions

View file

@ -26,7 +26,7 @@
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-icons/1.9.1/font/bootstrap-icons.min.css"
rel="stylesheet">
<link href="/static/paste.css" rel="stylesheet">
<link href="static/paste.css" rel="stylesheet">
</head>
<body>
<nav class="navbar sticky-top navbar-expand-lg navbar-dark bg-dark" id="navbar">
@ -179,7 +179,7 @@
This service is primarily designed for own usage and interest only.<br>
All data may be deleted or expired without any notification and guarantee. Please <b>DO NOT</b> abuse this
service.
The limit for file upload is <b>10 MB</b> and the paste will be kept for <b>28 days</b> only by default.<br>
The limit for file upload is <b>250 MB</b> and the paste will be kept for <b>28 days</b> only by default.<br>
The source code is available in my GitHub repository <a
href="https://github.com/rikkaneko/paste">[here]</a>.<br>
This webpage is designed for upload files only.
@ -271,7 +271,8 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.11.6/umd/popper.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/js/bootstrap.min.js"></script>
<script src="/static/paste.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>
<script src="static/paste.js"></script>
</body>
</html>

View file

@ -18,6 +18,8 @@
/// <reference path="../../node_modules/@types/bootstrap/index.d.ts" />
const ENDPOINT = '';
let input_div = {
file: null,
text: null,
@ -113,6 +115,16 @@ function build_paste_modal(paste_info, show_qrcode = true, saved = true, build_o
if (!build_only) modal.show();
}
/**
* @param file {File}
* @returns {str}
*/
async function get_file_hash(file) {
const word_arr = CryptoJS.lib.WordArray.create(await file.arrayBuffer());
const file_hash = CryptoJS.SHA256(word_arr).toString(CryptoJS.enc.Hex);
return file_hash;
}
$(function () {
input_div.file = $('#file_upload_layout');
input_div.text = $('#text_input_layout');
@ -170,11 +182,11 @@ $(function () {
title.val(this.files[0]?.name || '');
file_stat.text(`${this.files[0]?.type || 'application/octet-stream'}, ${size}`);
// Check length
if (bytes > 10485760) {
// Check length <= 250MB
if (bytes > 262144000) {
inputs.file.addClass('is-invalid');
file_stat.addClass('text-danger');
file_stat.text('The uploaded file is larger than the 10 MB limit.');
file_stat.text('The uploaded file is larger than the 250MB limit.');
}
});
@ -216,6 +228,7 @@ $(function () {
const form = $('#upload_form')[0];
let formdata = new FormData(form);
const type = formdata.get('paste-type');
/** @type {File} */
const content = formdata.get('u');
inputs[type].trigger('input');
@ -230,45 +243,95 @@ $(function () {
return false;
}
switch (type) {
case 'file':
formdata.set('paste-type', 'paste');
break;
case 'text':
formdata.set('paste-type', 'paste');
break;
case 'url':
formdata.set('paste-type', 'link');
}
upload_button.prop('disabled', true);
upload_button.text('Waiting...');
// Hanlde large paste (> 10MB)
if (content.size > 10485760) {
const file_hash = await get_file_hash(content);
const params = {
title: content.name,
'file-size': content.size,
'file-sha256-hash': file_hash,
'mime-type': content.type,
'read-limit': formdata.get('read-limit') || undefined,
pass: formdata.get('pass') || undefined,
};
// Remove empty entries
const filtered = new FormData();
Object.entries(params).forEach(([key, val]) => {
if (val) filtered.set(key, val);
});
try {
// Retrieve presigned URL for upload large paste
const res = await fetch(`${ENDPOINT}/v2/large_upload/create`, {
method: 'POST',
body: filtered,
});
if (!res.ok) {
throw new Error(`Unable to create paste: ${(await res.text()) || `${res.status} ${res.statusText}`}`);
}
// Upload the paste to the endpoint
upload_button.text('Uploading...');
const create_result = await res.json();
const res1 = await fetch(create_result.signed_url, {
method: 'PUT',
headers: {
'X-Amz-Content-Sha256': file_hash,
},
body: content,
});
if (!res1.ok) {
throw new Error(`Unable to upload paste: ${(await res1.text()) || `${res.status} ${res.statusText}`}`);
}
// Finialize the paste
const res2 = await fetch(`${ENDPOINT}/large_upload/complete/${create_result.uuid}`, {
method: 'POST',
});
if (res2.ok) {
const complete_result = await res2.json();
build_paste_modal(complete_result.paste_info, show_qrcode);
show_pop_alert(`Paste #${complete_result.paste_info.uuid} created!`, 'alert-success');
pass_input.val('');
} else {
throw new Error(`Unable to finialize paste: ${(await res2.text()) || `${res.status} ${res.statusText}`}`);
}
} catch (err) {
console.log('error', err);
show_pop_alert(err.message, 'alert-danger');
}
} else {
// Handle normal paste (<= 25MB)
// Remove empty entries
let filtered = new FormData();
formdata.forEach((val, key) => {
if (val) filtered.set(key, val);
});
// Request JSON response
filtered.set('json', '1');
upload_button.prop('disabled', true);
upload_button.text('Uploading...');
try {
const res = await fetch('/', {
const res = await fetch(`${ENDPOINT}/`, {
method: 'POST',
body: filtered,
});
if (res.ok) {
const paste_info = await res.json();
show_pop_alert('Paste created!', 'alert-success');
pass_input.val('');
build_paste_modal(paste_info, show_qrcode);
show_pop_alert(`Paste #${paste_info.uuid} created!`, 'alert-success');
pass_input.val('');
} else {
show_pop_alert('Unable to create paste', 'alert-warning');
throw new Error('Unable to upload paste');
}
} catch (err) {
console.log('error', err);
show_pop_alert(err.message, 'alert-danger');
}
}
upload_button.prop('disabled', false);
upload_button.text('Upload');
@ -304,7 +367,7 @@ $(function () {
}
try {
const res = await fetch(`/${uuid}/settings?${new URLSearchParams({ json: '1' })}`);
const res = await fetch(`${ENDPOINT}/${uuid}/settings?${new URLSearchParams({ json: '1' })}`);
if (res.ok) {
const paste_info = await res.json();
build_paste_modal(paste_info, show_qrcode, false);