diff --git a/README.md b/README.md index 8bed588..3453b89 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,6 @@ I'm making this to replace my current File uploader hosted on https://ayaya.beau ## TODO -- Add file size limit \ No newline at end of file +- ~~Add file size limit~~ ADDED +- Fix error when accessing `http://127.0.0.1:8080` with an empty DB. +- Better frontend... \ No newline at end of file diff --git a/config/config.yml b/config/config.yml deleted file mode 100644 index 060464d..0000000 --- a/config/config.yml +++ /dev/null @@ -1,2 +0,0 @@ -blocked_extensions: - - "exe" \ No newline at end of file diff --git a/public/chatterino.png b/public/chatterino.png new file mode 100644 index 0000000..d315edd Binary files /dev/null and b/public/chatterino.png differ diff --git a/public/script.js b/public/script.js new file mode 100644 index 0000000..1a60a5b --- /dev/null +++ b/public/script.js @@ -0,0 +1,140 @@ +document.addEventListener("DOMContentLoaded", () => { + const dropArea = document.getElementById("drop-area"); + const fileInput = document.getElementById("fileElem"); + const uploadStatus = document.getElementById("upload-status"); + + // Prevent default drag behaviors + ["dragenter", "dragover", "dragleave", "drop"].forEach(eventName => { + dropArea.addEventListener(eventName, preventDefaults, false); + document.body.addEventListener(eventName, preventDefaults, false); + }); + + // Highlight drop area when item is dragged over + ["dragenter", "dragover"].forEach(eventName => { + dropArea.addEventListener(eventName, highlight, false); + }); + + ["dragleave", "drop"].forEach(eventName => { + dropArea.addEventListener(eventName, unhighlight, false); + }); + + // Handle dropped files + dropArea.addEventListener("drop", handleDrop, false); + dropArea.addEventListener("click", () => fileInput.click()); + + // Handle file selection + fileInput.addEventListener("change", () => { + const files = fileInput.files; + handleFiles(files); + }, false); + + // Handle pasted files + document.addEventListener("paste", handlePaste, false); + + function preventDefaults(e) { + e.preventDefault(); + e.stopPropagation(); + } + + function highlight() { + dropArea.classList.add("highlight"); + } + + function unhighlight() { + dropArea.classList.remove("highlight"); + } + + function handleDrop(e) { + const dt = e.dataTransfer; + const files = dt.files; + handleFiles(files); + } + + function handlePaste(e) { + const items = e.clipboardData.items; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (item.kind === "file") { + const file = item.getAsFile(); + handleFiles([file]); + } + } + } + + function handleFiles(files) { + if (files.length > 0) { + for (const file of files) { + uploadFile(file); + } + } + } + + function uploadFile(file) { + const url = "upload"; // Replace with your upload URL + const xhr = new XMLHttpRequest(); + + // Create a new upload status container and link elements + const uploadContainer = document.createElement("div"); + const statusLink = document.createElement("div"); + const uploadText = document.createElement("span"); + const copyButton = document.createElement("button"); + + uploadContainer.className = "upload-status"; // Use the existing CSS class for styling + uploadContainer.appendChild(uploadText); + uploadContainer.appendChild(statusLink); + uploadContainer.appendChild(copyButton); + uploadStatus.appendChild(uploadContainer); // Append to the main upload status container + + // Update upload text + uploadText.innerHTML = "0%"; + uploadText.className = "percent" + copyButton.className = "copy-button"; // Add class for styling + copyButton.innerHTML = "Copy Link"; // Set button text + copyButton.style.display = "none"; // Hide initially + + // Update progress text + xhr.upload.addEventListener("progress", (e) => { + if (e.lengthComputable) { + const percentComplete = Math.round((e.loaded / e.total) * 100); + uploadText.innerHTML = `${percentComplete}%`; // Update the text with the percentage + } + }); + + // Handle response + xhr.onload = () => { + if (xhr.status === 200) { + try { + const response = JSON.parse(xhr.responseText); + const fileLink = response.link; // Assuming the response contains a key 'link' + statusLink.innerHTML = `${fileLink}`; + copyButton.style.display = "inline"; // Show the copy button + copyButton.onclick = () => copyToClipboard(fileLink); // Set the copy action + } catch (error) { + statusLink.textContent = "File uploaded but failed to parse response."; + } + } else { + statusLink.textContent = "File upload failed."; + } + }; + + // Handle errors + xhr.onerror = () => { + statusLink.textContent = "An error occurred during the file upload."; + }; + + // Send file + const formData = new FormData(); + formData.append("file", file); + xhr.open("POST", url, true); + xhr.send(formData); + } + + // Function to copy the link to the clipboard + function copyToClipboard(text) { + navigator.clipboard.writeText(text).then(() => { + // alert("Link copied to clipboard!"); // Notify the user + }).catch(err => { + console.error("Failed to copy: ", err); + }); + } +}); diff --git a/public/sharex.sxcu b/public/sharex.sxcu new file mode 100644 index 0000000..2f14991 --- /dev/null +++ b/public/sharex.sxcu @@ -0,0 +1,11 @@ +{ + "Version": "14.0.1", + "DestinationType": "ImageUploader, FileUploader", + "RequestMethod": "POST", + "RequestURL": "https://ayaya.beauty/upload", + "Body": "MultipartFormData", + "FileFormName": "file", + "URL": "{json:link}", + "DeletionURL": "{json:deleteLink}", + "ErrorMessage": "{json:error}" +} \ No newline at end of file diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..f48fa5a --- /dev/null +++ b/public/styles.css @@ -0,0 +1,135 @@ +body { + font-family: Arial, sans-serif; + background-color: #111; + margin: 0; + padding: 20px; +} + +p, h1, h2, h3, h4, h5 { + color: aliceblue +} + +.percent { + color: aliceblue +} + +.container { + max-width: 700px; + margin: auto; +/* background: white; */ + padding: 20px; + border-radius: 0px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + +#drop-area { + /*! border: 2px solid #00ff00; */ + /*! border-radius: 6px; */ + /*! padding-left: 10px; */ + /*! padding-right: 10px; */ + text-align: center; + position: relative; + width: fit-content; + margin: 0 auto; /* Center the element */ + display: block; /* Ensure it behaves as a block-level element */ + background: rgba(202,230,190,.75); + border: 1px solid #b7d1a0; + border-radius: 4px; + color: #468847; + cursor: pointer; + /*! display: inline-block; */ + font-size: 24px; + padding: 28px 48px; + text-shadow: 0 1px hsla(0,0%,100%,.5); + transition: background-color .25s,width .5s,height .5s; +} + + +.button { + display: inline-block; + padding: 10px 20px; +/* background: #; */ + color: white; + border-radius: 5px; + cursor: pointer; +/* margin-top: 10px; */ +} + +.upload-status { + margin-top: 10px; +} + +.copy-button { + margin-top: 5px; + padding: 5px 10px; + background: #28a745; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + display: none; /* Hidden initially */ +} + +nav a, nav > ul +{ + list-style: none; + margin: 0; + padding: 0; + text-align: center; +} + +#upload-status { + margin: 20px; /* Adjust as needed */ +} + +.upload-status { + display: flex; + align-items: center; + justify-content: space-between; + border: 2px solid #f40101; /* Optional styling for the status box */ + padding: 10px; /* Optional padding */ + border-radius: 6px; /* Optional rounded corners */ + /*! background-color: #f9f9f9; */ /* Optional background color */ +} + +.link-container { + display: flex; + align-items: center; + margin-left: auto; /* Pushes the link and button to the right */ +} + +.link { + color: #ffb6c1; + text-decoration: none; /* Remove underline from link */ + margin-right: 5px; /* Space between link and button */ +} + +.link:hover { + text-decoration: underline; /* Optional: underline on hover */ +} + +.copy-button { + display: inline; + background-color: #5e5e5e; /* Button background color */ + color: white; /* Button text color */ + border: none; /* Remove border */ + border-radius: 3px; /* Rounded corners for the button */ + padding: 5px 10px; /* Button padding */ + cursor: pointer; /* Pointer cursor on hover */ +} + +.copy-button:hover { + background-color: #404040; /* Darker shade on hover */ +} + +a:link { + color: #ffb6c1 +} + +a:visited { + color: #ffb6c1 +} + +a:hover { + color: #ffb6c1 +} \ No newline at end of file diff --git a/public/upload.js b/public/upload.js new file mode 100644 index 0000000..26241c6 --- /dev/null +++ b/public/upload.js @@ -0,0 +1,165 @@ +// document.addEventListener("DOMContentLoaded", () => { +// const dropArea = document.getElementById("drop-area"); +// const fileInput = document.getElementById("fileElem"); +// const progressContainer = document.getElementById("progress-container"); +// const progressBar = document.getElementById("progress-bar"); +// const status = document.getElementById("status"); + +// // Prevent default drag behaviors +// ["dragenter", "dragover", "dragleave", "drop"].forEach(eventName => { +// dropArea.addEventListener(eventName, preventDefaults, false); +// document.body.addEventListener(eventName, preventDefaults, false); +// }); + +// // Highlight drop area when item is dragged over +// ["dragenter", "dragover"].forEach(eventName => { +// dropArea.addEventListener(eventName, highlight, false); +// }); + +// ["dragleave", "drop"].forEach(eventName => { +// dropArea.addEventListener(eventName, unhighlight, false); +// }); + +// // Handle dropped files +// dropArea.addEventListener("drop", handleDrop, false); +// dropArea.addEventListener("click", () => fileInput.click()); + +// // Handle file selection +// fileInput.addEventListener("change", () => { +// const files = fileInput.files; +// handleFiles(files); +// }, false); + +// // Handle pasted files +// document.addEventListener("paste", handlePaste, false); + +// function preventDefaults(e) { +// e.preventDefault(); +// e.stopPropagation(); +// } + +// function highlight() { +// dropArea.classList.add("highlight"); +// } + +// function unhighlight() { +// dropArea.classList.remove("highlight"); +// } + +// function handleDrop(e) { +// const dt = e.dataTransfer; +// const files = dt.files; +// handleFiles(files); +// } + +// function handlePaste(e) { +// const items = e.clipboardData.items; +// for (let i = 0; i < items.length; i++) { +// const item = items[i]; +// if (item.kind === "file") { +// const file = item.getAsFile(); +// handleFiles([file]); +// } +// } +// } + +// function handleFiles(files) { +// if (files.length > 0) { +// uploadFile(files[0]); +// } +// } + +// function uploadFile(file) { +// const url = "upload"; // Replace with your upload URL +// const xhr = new XMLHttpRequest(); + +// // Update progress bar +// xhr.upload.addEventListener("progress", (e) => { +// if (e.lengthComputable) { +// const percentComplete = (e.loaded / e.total) * 100; +// progressBar.style.width = percentComplete + "%"; // Set the width of the progress bar +// progressContainer.style.display = "block"; // Show progress container +// } +// }); + +// // Handle response +// xhr.onload = () => { +// if (xhr.status === 200) { +// try { +// const response = JSON.parse(xhr.responseText); +// const fileLink = response.link; // Assuming the response contains a key 'link' +// status.innerHTML = `File uploaded successfully! Click here to view the file`; +// } catch (error) { +// status.textContent = "File uploaded but failed to parse response."; +// } +// } else { +// status.textContent = "File upload failed."; +// } +// progressBar.style.width = "0"; // Reset progress bar +// progressContainer.style.display = "none"; // Hide progress container +// }; + +// // Handle errors +// xhr.onerror = () => { +// status.textContent = "An error occurred during the file upload."; +// progressBar.style.width = "0"; // Reset progress bar +// progressContainer.style.display = "none"; // Hide progress container +// }; + +// // Send file +// const formData = new FormData(); +// formData.append("file", file); +// xhr.open("POST", url, true); +// xhr.send(formData); +// } +// }); + +function handleFiles(input) { + const files = input.files; + Array.from(files).forEach(file => { + // Display download link initially + document.querySelector(`#link-${file.name}`).textContent = "Uploading..."; + + // Create a new FormData instance + let formData = new FormData(); + formData.append('file', file); + + // Simulate a request to the server + fetch('/upload', { method: 'POST', body: formData }) + .then(response => response.json()) + .then(data => { + // Update the progress bar + document.querySelector(`#progress-${file.name}`).style.width = `${data.progress}%`; + + // Display the download link + document.querySelector(`#link-${file.name}`).textContent = data.link; + }) + .catch(error => console.error('Error:', error)); + }); +} + +// Handle drag & drop +document.addEventListener('dragover', function(event) { + event.preventDefault(); + event.stopPropagation(); +}); + +document.addEventListener('drop', function(event) { + event.preventDefault(); + event.stopPropagation(); + + const files = event.dataTransfer.files; + handleFiles(files); +}, false); + +// Handle clipboard paste +document.addEventListener('paste', function(event) { + event.preventDefault(); + event.stopPropagation(); + + const items = event.clipboardData.items; + if (items.length > 0 && items[0].type.indexOf("text") !== -1) { + const file = items[0].getAsFile(); + handleFiles([file]); + } +}, false); diff --git a/src/config.cr b/src/config.cr index 64b59c8..ade0e06 100644 --- a/src/config.cr +++ b/src/config.cr @@ -4,6 +4,7 @@ class Config include YAML::Serializable property files : String = "./files" + property secure : Bool = false property db : String = "./db.sqlite3" property filename_lenght : Int8 = 3 # In MiB @@ -16,6 +17,8 @@ class Config property delete_key_lenght : Int8 = 8 # Blocked extensions that are not allowed to be uploaded to the server property blocked_extensions : Array(String) = [] of String + property siteInfo : String = "xd" + property siteWarning : String? = "" def self.load config_file = "config/config.yml" diff --git a/src/file-uploader.cr b/src/file-uploader.cr index 6c30bdd..c704481 100644 --- a/src/file-uploader.cr +++ b/src/file-uploader.cr @@ -14,10 +14,16 @@ CONFIG = Config.load Kemal.config.port = CONFIG.port SQL = DB.open("sqlite3://#{CONFIG.db}") +# https://github.com/iv-org/invidious/blob/90e94d4e6cc126a8b7a091d12d7a5556bfe369d5/src/invidious.cr#L78 +CURRENT_BRANCH = {{ "#{`git branch | sed -n '/* /s///p'`.strip}" }} +CURRENT_COMMIT = {{ "#{`git rev-list HEAD --max-count=1 --abbrev-commit`.strip}" }} +CURRENT_VERSION = {{ "#{`git log -1 --format=%ci | awk '{print $1}' | sed s/-/./g`.strip}" }} + Utils.create_db Utils.create_files_dir get "/" do |env| + host = env.request.headers["Host"] render "src/views/index.ecr" end @@ -47,3 +53,7 @@ end CHECK_OLD_FILES.enqueue Kemal.run + +{% if flag?(:release) || flag?(:production) %} + Kemal.config.env = "production" if !ENV.has_key?("KEMAL_ENV") +{% end %} diff --git a/src/handling.cr b/src/handling.cr index 26b51cd..bb04397 100644 --- a/src/handling.cr +++ b/src/handling.cr @@ -56,7 +56,8 @@ end if !filename.empty? JSON.build do |j| j.object do - j.field "link", "https://#{env.request.headers["Host"]}/#{filename + extension}" + CONFIG.secure ? j.field "link", "https://#{env.request.headers["Host"]}/#{filename}" : j.field "link", "http://#{env.request.headers["Host"]}/#{filename}" + j.field "linkExt", "https://#{env.request.headers["Host"]}/#{filename}#{extension}" j.field "id", filename j.field "ext", extension j.field "name", original_filename @@ -71,6 +72,7 @@ end end def retrieve_file(env) + puts env.params.url filename = SQL.query_one "SELECT filename FROM files WHERE filename = ?", env.params.url["filename"].to_s.split(".").first, as: String extension = SQL.query_one "SELECT extension FROM files WHERE filename = ?", filename, as: String send_file env, "#{CONFIG.files}/#{filename}#{extension}" @@ -102,7 +104,7 @@ end file_extension = SQL.query_one "SELECT extension FROM files WHERE delete_key = ?", env.params.query["key"], as: String File.delete("#{CONFIG.files}/#{file_to_delete}#{file_extension}") SQL.exec "DELETE FROM files WHERE delete_key = ?", env.params.query["key"] - msg("File deleted successfully") + msg("File '#{file_to_delete}' deleted successfully") rescue ex error403("Unknown error: #{ex.message}") end diff --git a/src/views/index.ecr b/src/views/index.ecr index 3817386..3dae6d6 100644 --- a/src/views/index.ecr +++ b/src/views/index.ecr @@ -1,100 +1,53 @@ + - - - - File Upload - + + + <%= host %> + - -
-
-
-

Drag and drop a file here or click to select

- -
- - - -
-
-
- -
-
- - +
+

<%= host %>

+

<%= CONFIG.siteInfo %>

+
+

Drop or Choose file(s)

+ + +
+
+
+
+
+

Chatterino Config

+

ShareX Config

+

file-uploader-crystal (BETA <%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %> @ <%= CURRENT_BRANCH %>)

+
+ - - \ No newline at end of file +