Compare commits

...

No commits in common. "main" and "Adt2" have entirely different histories.
main ... Adt2

20 changed files with 671 additions and 709 deletions

View file

@ -1,11 +0,0 @@
.PHONY: fileshare clean
fileshare:
rm -rf server/dist
cp -r client server/dist
cd server && go build -ldflags "-s -w"
mv server/fileshare fileshare
clean:
rm -rf server/dist
rm fileshare

View file

@ -1,9 +0,0 @@
{
"semi": false,
"trailingComma": "none",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true
}

View file

@ -1,22 +0,0 @@
/* latin-ext */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 100 800;
font-display: swap;
src: url(/assets/fonts/JetBrains_Mono_latin-ext.woff2) format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 100 800;
font-display: swap;
src: url(/assets/fonts/JetBrains_Mono_latin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

View file

@ -1,183 +0,0 @@
document.addEventListener('DOMContentLoaded', async () => {
const urlParams = new URLSearchParams(window.location.search)
const fileId = urlParams.get('id')
const key = urlParams.get('key')
if (fileId && key) {
displayFileDetails(fileId, key)
} else {
document.getElementById('upload__form').style.display = 'block'
setupUploadForm()
}
})
const baseUrl = window.location.origin
const CHUNK_SIZE = 100 * 1024 * 1024 // 100 MB
async function displayFileDetails(fileId, key) {
try {
const response = await fetch(`${baseUrl}/get/${fileId}?key=${key}`, {
method: 'GET'
})
const fileDetails = document.getElementById('file__details')
const fileNameElement = document.getElementById('file__name')
const fileSizeElement = document.getElementById('file__size')
const downloadButton = document.getElementById('download__btn')
const copyButton = document.getElementById('copy__btn')
if (!response.ok) {
fileDetails.textContent = `Error: ${response.statusText}`
fileDetails.style.display = 'flex'
return
}
const contentType = response.headers.get('Content-Type')
if (contentType && contentType.includes('application/json')) {
const result = await response.json()
const downloadUrl = `${baseUrl}/download/${fileId}?key=${key}`
const pageUrl = `${baseUrl}/?id=${fileId}&key=${key}`
fileNameElement.textContent = `${result.fileName}`
fileSizeElement.textContent = `${result.fileSize}`
downloadButton.innerHTML = `<a href="${downloadUrl}">Download</a>`
copyButton.onclick = () => {
navigator.clipboard.writeText(pageUrl)
copyButton.textContent = 'Copied!'
}
} else {
const result = await response.text()
fileDetails.textContent = result
}
fileDetails.style.display = 'flex'
} catch (error) {
console.error('Error:', error)
document.getElementById('file__details').textContent =
'An error occurred. Please try again.'
document.getElementById('file__details').style.display = 'flex'
}
}
function setupUploadForm() {
const fileInput = document.querySelector('.upload__input')
const overlayText = document.querySelector('.upload__input__overlay__text')
fileInput.addEventListener('change', () => {
if (fileInput.files.length > 0) {
overlayText.textContent = fileInput.files[0].name
} else {
overlayText.textContent = 'Choose a file or drag it here'
}
})
document
.getElementById('upload__form')
.addEventListener('submit', async (event) => {
event.preventDefault()
const file = fileInput.files[0]
if (!file) {
console.log('No file selected.')
return
}
const progressBar = document.getElementById('upload__progress')
const progressFill = document.getElementById('progress__fill')
const uploadButton = document.getElementById('upload__btn')
uploadButton.style.display = 'none'
progressBar.style.display = 'block'
fileInput.disabled = true
try {
await uploadFileInChunks(file, progressFill)
} catch (error) {
console.error('Error:', error)
document.getElementById('upload__result').textContent =
'An error occurred. Please try again.'
document
.getElementById('upload__result')
.classList.add('upload__result__visible')
} finally {
progressBar.style.display = 'none'
uploadButton.style.display = 'inline-block'
fileInput.disabled = false
}
})
}
async function uploadFileInChunks(file, progressFill) {
const fileSize = file.size
const chunkCount = Math.ceil(fileSize / CHUNK_SIZE)
const uploadId = generateUploadId()
let uploadedSize = 0
for (let chunkIndex = 0; chunkIndex < chunkCount; chunkIndex++) {
const start = chunkIndex * CHUNK_SIZE
const end = Math.min(start + CHUNK_SIZE, fileSize)
const chunk = file.slice(start, end)
const formData = new FormData()
formData.append('chunk', chunk)
formData.append('uploadId', uploadId)
formData.append('chunkIndex', chunkIndex)
formData.append('chunkCount', chunkCount)
formData.append('fileName', file.name)
await uploadChunk(formData, progressFill, uploadedSize, fileSize)
uploadedSize += chunk.size
}
// Call upload_complete endpoint
const completeFormData = new FormData()
completeFormData.append('uploadId', uploadId)
completeFormData.append('chunkCount', chunkCount)
completeFormData.append('fileName', file.name)
const completeResponse = await fetch(`${baseUrl}/upload_complete`, {
method: 'POST',
body: completeFormData
})
if (!completeResponse.ok) {
throw new Error(`Error completing upload: ${completeResponse.statusText}`)
}
const result = await completeResponse.json()
const pageUrl = `${baseUrl}/?id=${result.id}&key=${result.key}`
window.location.href = pageUrl
}
async function uploadChunk(formData, progressFill, uploadedSize, fileSize) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('POST', `${baseUrl}/upload_chunk`, true)
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
const totalUploaded = uploadedSize + event.loaded
const progress = Math.round((totalUploaded / fileSize) * 100)
progressFill.style.width = `${progress}%`
}
}
xhr.onload = () => {
if (xhr.status === 200) {
resolve()
} else {
reject(new Error(`Error uploading chunk: ${xhr.statusText}`))
}
}
xhr.onerror = () => reject(new Error('Network error occurred'))
xhr.send(formData)
})
}
function generateUploadId() {
return Math.random().toString(36).substr(2, 9)
}

View file

@ -1,179 +0,0 @@
@import url('fonts.css');
body {
background-color: #282828;
color: #ebdbb2;
margin: 0;
padding: 0;
font-family: 'JetBrains Mono', monospace;
}
#root {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
width: auto;
}
.header {
display: flex;
justify-content: center;
padding: 1rem;
}
.upload__container,
.file__details {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 600px;
min-width: 600px;
border-radius: 2rem;
gap: 1rem;
padding: 1rem;
background-color: #ebdbb2;
color: #282828;
overflow-wrap: anywhere;
position: relative;
}
.file__details__text {
font-size: 1.5rem;
}
.file__details__button__container {
display: flex;
gap: 1rem;
}
.upload__button,
.download__button {
font-size: 1.5rem;
padding: 1rem;
background-color: #282828;
border-radius: 16px;
border: 1px solid #282828;
color: #ebdbb2;
cursor: pointer;
transition: transform 0.3s ease-in-out;
}
.upload__button:hover,
.download__button:hover {
transform: scale(1.1);
}
.upload__button {
position: absolute;
bottom: 5.75%;
}
.upload__info {
font-size: 0.8rem;
color: #454545;
}
.upload__input {
color: #282828;
border: 2px solid #282828;
padding: 2rem;
border-radius: 0.5rem;
min-height: 300px;
min-width: 300px;
position: absolute;
left: 50%;
transform: translateX(-50%);
opacity: 0;
z-index: 2;
cursor: pointer;
}
.upload__input__overlay {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
left: 50%;
transform: translateX(-50%);
color: #282828;
border: 2px solid #282828;
padding: 2rem;
border-radius: 0.5rem;
min-height: 300px;
min-width: 300px;
}
.upload__input__overlay__text {
text-align: center;
}
.upload__result {
opacity: 0;
padding: 1rem;
}
.upload__result__visible {
opacity: 1;
}
.upload__progress {
width: 90%;
height: 20px;
background-color: #a89984;
border-radius: 8px;
overflow: hidden;
margin: 1rem;
position: absolute;
bottom: 5%;
}
.upload__progress__fill {
height: 100%;
width: 0;
background-color: #504945;
transition: width 0.2s ease-in-out;
}
a {
text-decoration: none;
color: inherit;
}
@media (max-width: 768px) {
.upload__container,
.file__details {
min-height: 400px;
min-width: 300px;
max-height: 400px;
max-width: 300px;
}
.upload__input,
.upload__input__overlay {
min-height: 100px;
min-width: 250px;
max-width: 250px;
padding: 0.5rem;
}
.upload__input__overlay__text {
font-size: 0.8rem;
}
.upload__info {
font-size: 0.6rem;
}
.upload__button,
.download__button {
font-size: 1.2rem;
}
.file__details__text {
font-size: 1.2rem;
}
}

View file

@ -1,48 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>File Share</title>
<link rel="stylesheet" href="assets/styles.css" />
</head>
<body>
<div id="root">
<div class="header">
<h2 class="header__title">File Share</h2>
</div>
<form id="upload__form" style="display: none">
<div class="upload__container">
<div class="upload__input__overlay">
<p class="upload__input__overlay__text">
Choose a file or drag it here<br><span class="upload__info">(It will be deleted after 24 hours.)</span>
</p>
</div>
<input type="file" class="upload__input" name="file" />
<div
class="upload__progress"
id="upload__progress"
style="display: none"
>
<div class="upload__progress__fill" id="progress__fill"></div>
</div>
<button type="submit" class="upload__button" id="upload__btn">
Upload
</button>
</div>
</form>
<div class="file__details" id="file__details" style="display: none">
<p class="file__details__text" id="file__name"></p>
<p class="file__details__text" id="file__size"></p>
<div class="file__details__button__container">
<button class="download__button" id="download__btn">Download</button>
<button class="download__button" id="copy__btn">Copy Link</button>
</div>
</div>
<div class="upload__result" id="upload__result">Error Placeholder</div>
</div>
<script src="assets/index.js"></script>
</body>
</html>

BIN
server/dist/Poppins-Light.ttf vendored Normal file

Binary file not shown.

BIN
server/dist/Poppins-Medium.ttf vendored Normal file

Binary file not shown.

134
server/dist/fileinfo.css vendored Normal file
View file

@ -0,0 +1,134 @@
@font-face {
font-family: Poppins-Light ;
src: url(Poppins-Light.ttf);
}
@font-face {
font-family: Poppins-Medium;
src: url(Poppins-Medium.ttf);
}
*{
margin: 0px;
}
#navbar{
width: 2000px;
height: 100px;
background-color: #5bc703;
margin-top: 0px;
}
h1{
font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
color: rgb(253, 253, 253);
font-size: 50px;
margin-left: 50px;
margin-top: -80px;
}
body{
background-color: #383836;
}
#main{
background-color: #5bc703;
height: 400px;
width: 800px;
padding: 100px;
margin-left: 400px;
margin-top: 5%;
border-radius: 30px;
border: 4px dashed white;
}
#undiv1{
height: 200px;
width: 500px;
padding: 50px;
margin-left: 10px;
}
#undiv2{
height: 200px;
width: 600px;
padding: 20px;
margin-left: 70px;
margin-top: -70px;
}
#idInput{
font-size: 30px;
font-family: Poppins-Medium;
background-color: #f3f5f7;
border-radius: 10px;
border-color: #294F6A;
}
#lab1{
font-size: 30px;
color: white;
}
#keyInput{
font-size: 30px;
font-family: Poppins-Medium;
background-color: #f7f9fa;
border-radius: 10px;
border-color: #294F6A;
}
#lab2{
font-size: 30px;
color: white;
}
#btn1{
font-size: 20px;
border-radius: 10px;
padding: 10px 10px 10px 10px;
cursor: pointer;
background-color: #294F6A;
border-color: #294F6A;
margin-left: 100px;
}
#link{
color: antiquewhite;
text-decoration: solid;
}
#btn2{
font-size: 20px;
border-radius: 10px;
padding: 10px 10px 10px 10px;
cursor: pointer;
background-color: #294F6A;
border-color: #294F6A;
margin-left: -30px;
color: antiquewhite;
}
#fileInfo{
color: #5bc703;
height: 150px;
width: 800px;
background-color: #ffffff;
margin-top: 100px;
border-radius: 20px;
justify-content: center;
border: 3px dashed #5bc703;
font-size: 30px;
font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
text-align: left;
padding: 40px 40px 40px 40px;
display: none;
}
#footer{
height: 100px;
background-color: #5bc703;
margin-top: 20%;
text-align: center;
color: rgb(247, 245, 242);
padding: 40px 40px 40px 40px;
}

43
server/dist/fileinfo.html vendored Normal file
View file

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FILE SHARE</title>
<link rel="stylesheet" href="fileinfo.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
</head>
<body>
<div id="navbar">
</div>
<h1> FILE SHARE </h1>
<div id="main">
<div id="undiv1">
<label for="idInput" id="lab1">Enter ID:</label>
<input type="text" id="idInput"><br><br>
<label for="keyInput" id="lab2">Enter Key:</label>
<input type="text" id="keyInput"><br><br>
</div>
<div id="undiv2">
<button id= "btn2" onclick="getID()">Fetch Data</button>
<button id="btn1"><a id="link"> Download</a></button>
</div>
<div id="fileInfo"> </div>
</div>
<footer id="footer"> @Fileshare</footer>
<script src="fileinfo.js"></script>
</body>
</html>

36
server/dist/fileinfo.js vendored Normal file
View file

@ -0,0 +1,36 @@
const fileInfo = document.querySelector("#fileInfo");
let link = document.getElementById('link')
async function getID() {
const id = document.getElementById('idInput').value;
const apiKey = document.getElementById('keyInput').value;
const baseURL1 = 'http://localhost:8080/get';
const baseURL2 = 'http://localhost:8080/download';
const url = `${baseURL1}/${id}?key=${apiKey}`;
link.href =`${baseURL2}/${id}?key=${apiKey}` ;
try {
let response = await fetch(url);
let data = await response.json();
fileName = data.fileName;
fileSize = data.fileSize;
fileInfo.innerHTML = `<b>File Name:</b> ${fileName}<br><br></b><b>File Size:</> </b>${fileSize}`;
fileInfo.style.display = 'block';
}
catch (error) {
console.error('Error fetching data:', error);
}
}

BIN
server/dist/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

226
server/dist/upload.css vendored Normal file
View file

@ -0,0 +1,226 @@
@font-face {
font-family: Poppins-Light ;
src: url(Poppins-Light.ttf);
}
@font-face {
font-family: Poppins-Medium;
src: url(Poppins-Medium.ttf);
}
*{
margin-left: 0px;
margin-bottom: 100px;
margin-top: 0px;
}
body{
background-color:#383836;
display: flex;
}
#navbar{
width: 2000px;
height: 100px;
background-color: #5bc703;
margin-top: 0px;
}
#logo{
height: 90px;
width: 90px;
margin-top: 50px;
margin-left: 20px;
}
#uploader-title{
font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
color: rgb(249, 250, 253);
margin-left: -1950px;
margin-top: 20px;
font-size: 50px;
padding-left: 0px;
}
.file-uploader{
height: 550px;
width: 800px;
margin-top: 30%;
margin-left: 15%;
background-color:#5bc703;
border-radius: 30px;
padding: 2px;
border: 4px dashed white;
}
.file-instruction{
color: rgb(232, 241, 241);
margin-left: 50px;
font-family: "Poppins-Light";
font-size: 15px;
font-weight: lighter;
margin-top: 100px;
}
.file-upload-box{
height: 200px;
width: 300px;
margin-left: 230px;
margin-top: 100px;
margin-right: 500px;
background-color:#5bc703;
border: 2px dashed rgb(209, 209, 209);
border-radius: 30px;
padding: 20px;
}
.uploadsec{
margin-left: 100px;
margin-top: 30%;
}
#uploadbtn{
font-size: 25px;
min-width: max-content;
padding: 5px 30px 5px 30px ;
margin-left: -30px;
margin-top: 100px;
background-color:#294F6A;
border-radius: 20px;
color: white;
border-color:#294F6A ;
cursor: pointer;
}
#fileInput{
padding: 5px 30px 5px 30px ;
font-size: 17px;
margin-left: -70px;
margin-top: 30px;
color: white;
}
#i2{
margin-top: 500px;
padding-right: 50px;
margin-left: 200px;
color: blueviolet;
}
#i3{
margin-top: 600px;
padding-right: 50px;
margin-left: 300px;
color: blueviolet;
}
#boxmain{
margin-left: 400px;
font-size: 50px;
margin-top: 100px;
color: aliceblue;
font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
text-shadow: 3px 3px 4px rgb(73, 50, 73);
}
#pdnd{
color: #475c5a;
margin-left: 90px;
font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
}
#footer{
margin-top: 500px;
}
#myDropzone{
background-color: #294F6A;
border: #294F6A;
color: white;
font-size: 20px;
font-family: Poppins-Light;
}
.info1{
height: 50px;
width: 700px;
background-color: #ffffff;
margin-top: -100px;
margin-bottom: 100px;
padding-bottom: 20px;
margin-left: -600px;
border-radius: 25px;
text-align: center;
justify-content: center; /* Aligns horizontally */
align-items: center;
font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
display: none;
color: #437718;
padding: 10px 10px 10px 10px;
}
.info1v{
height: 50px;
width: 700px;
background-color: #ffffff;
margin-top: -100px;
margin-bottom: 100px;
padding-bottom: 20px;
margin-left: -600px;
border-radius: 25px;
text-align: center;
justify-content: center; /* Aligns horizontally */
align-items: center;
font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
display: block;
border: 3px dashed #5bc703;
text-align: left;
padding: 10px 10px 10px 10px;
color: #437718;
}
.info2{
height: 50px;
width: 700px;
background-color: #ffffff;
margin-top: -100px;
margin-left: 150px;
border-radius: 25px;
text-align: center;
font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
display: flex;
justify-content: center;
align-items: center;
display: none;
color:#437718;
padding: 10px 10px 10px 10px;
}
.info2v{
height: 50px;
width: 700px;
background-color: #ffffff;
margin-top: -180px;
margin-left: 150px;
border-radius: 25px;
text-align: center;
font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
display: flex;
justify-content: center;
align-items: center;
display: block;
border: 3px dashed #5bc703;
text-align: left;
padding: 10px 10px 10px 10px;
color: #437718;
}
#footer{
height: 100px;
background-color: #5bc703;
margin-top: 20%;
text-align: center;
color: rgb(252, 250, 247);
padding: 40px 40px 40px 40px;
}

38
server/dist/upload.html vendored Normal file
View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>File Upload</title>
<link rel="stylesheet" href="upload.css">
<body>
<div id="navbar"></div>
<h2 id="uploader-title">FILE SHARE</h2>
<div id="main">
<div class="file-uploader">
<div class="uploader-header">
<h4 id="file-completed-status"></h4>
</div>
<ul class="file-list"></ul>
<div class="file-upload-box">
<h2 class="box-title">
</h2>
<div class="uploadsec">
<form id="uploadForm">
<input type="file" id="fileInput" name="file">
<button type="submit" id="uploadbtn" >Upload</button>
</form>
</div>
<div class="info1" id="info1h"></div>
<div class="info2" id="info2h"></div>
</div>
</div>
</div>
<script src="upload.js"></script>
</body>
</html>

51
server/dist/upload.js vendored Normal file
View file

@ -0,0 +1,51 @@
document.getElementById('uploadForm').addEventListener('submit', async (event) => {
event.preventDefault();
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
console.log('No file selected.');
return;
}
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('http://localhost:8080/upload', {
method: 'POST',
body: formData
});
const info1h = document.getElementById('info1h');
const info2h = document.getElementById('info2h');
if (response.ok) {
const contentType = response.headers.get('Content-Type');
if (contentType && contentType.includes('application/json')) {
const result = await response.json();
info1h.innerText = "id=" + result.id;
info2h.innerText = "key=" + result.key;
} else {
const result = await response.text();
info1h.innerText = result;
info2h.innerText = "";
}
info1h.classList.remove('info1');
info1h.classList.add('info1v');
info2h.classList.remove('info2');
info2h.classList.add('info2v');
} else {
info1h.innerText = response.statusText;
info1h.classList.remove('info1');
info1h.classList.add('info1v');
}
} catch (error) {
console.error('Error:', error);
}
});

View file

@ -1,4 +1,4 @@
module fileshare
module file-share
go 1.22.2

View file

@ -1,7 +1,6 @@
package main
import (
"bytes"
"context"
"crypto/aes"
"crypto/cipher"
@ -10,12 +9,9 @@ import (
"embed"
"encoding/hex"
"errors"
"flag"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
"github.com/labstack/echo/v4"
@ -24,37 +20,32 @@ import (
)
const (
maxUploadSize = 3 * 1024 * 1024 * 1024 // 3 GB
maxUploadSize = 10 * 1024 * 1024 // 10 MB
keySize = 32
nonceSize = 12
)
var db *sql.DB
var port string
//go:embed all:dist
var dist embed.FS
func registerHandlers(e *echo.Echo) {
e.Use(middleware.BodyLimit(fmt.Sprintf("%dM", maxUploadSize/(1024*1024))))
func RegisterHandlers(e *echo.Echo) {
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Root: "dist",
Index: "index.html",
Index: "upload.html",
HTML5: true,
Filesystem: http.FS(dist),
}))
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())
e.POST("/upload_chunk", handleUploadChunk)
e.POST("/upload_complete", handleUploadComplete)
e.POST("/upload", handleUpload)
e.GET("/download/:id", handleDownload)
e.GET("/get/:id", handleGetFileInfo)
}
func main() {
flag.StringVar(&port, "port", "8080", "HTTP server port")
flag.Parse()
var err error
db, err = initDB()
if err != nil {
@ -63,20 +54,15 @@ func main() {
defer db.Close()
e := echo.New()
registerHandlers(e)
startCleanupScheduler()
RegisterHandlers(e)
e.Logger.Fatal(e.Start(":" + port))
// Start server
e.Logger.Fatal(e.Start(":8080"))
}
func initDB() (*sql.DB, error) {
user := os.Getenv("POSTGRES_USER")
password := os.Getenv("POSTGRES_PASSWORD")
dbname := os.Getenv("POSTGRES_DB")
dbURL := fmt.Sprintf("postgres://%s:%s@localhost/%s?sslmode=disable", user, password, dbname)
db, err := sql.Open("postgres", dbURL)
db, err := sql.Open("postgres", "postgres://postgres:postfor24@localhost/filedb?sslmode=disable")
if err != nil {
return nil, err
}
@ -84,103 +70,60 @@ func initDB() (*sql.DB, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := createTables(ctx, db); err != nil {
if err := createFilesTable(ctx, db); err != nil {
return nil, err
}
return db, nil
}
func createTables(ctx context.Context, db *sql.DB) error {
func createFilesTable(ctx context.Context, db *sql.DB) error {
_, err := db.ExecContext(ctx, `
CREATE TABLE IF NOT EXISTS chunks (
upload_id TEXT,
chunk_index INT,
chunk_data BYTEA,
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (upload_id, chunk_index)
);
CREATE TABLE IF NOT EXISTS files (
id TEXT,
id TEXT PRIMARY KEY,
name TEXT,
chunk_index INT,
chunk_data BYTEA,
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (id, chunk_index)
data BYTEA
);
`)
return err
}
func handleUploadChunk(c echo.Context) error {
uploadId := c.FormValue("uploadId")
chunkIndex, err := strconv.Atoi(c.FormValue("chunkIndex"))
if err != nil {
return handleError(c, fmt.Errorf("invalid chunk index: %v", err), http.StatusBadRequest)
func handleUpload(c echo.Context) error {
r := c.Request()
if err := r.ParseMultipartForm(maxUploadSize); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": fmt.Sprintf("error parsing multipart form: %v", err)})
}
chunk, err := c.FormFile("chunk")
if err != nil {
return handleError(c, fmt.Errorf("error getting form file: %v", err), http.StatusBadRequest)
}
src, err := chunk.Open()
if err != nil {
return handleError(c, fmt.Errorf("error opening chunk: %v", err), http.StatusInternalServerError)
}
defer src.Close()
chunkData, err := io.ReadAll(src)
if err != nil {
return handleError(c, fmt.Errorf("error reading chunk data: %v", err), http.StatusInternalServerError)
}
if err := storeChunkInDB(c.Request().Context(), uploadId, chunkIndex, chunkData); err != nil {
return handleError(c, fmt.Errorf("error storing chunk in database: %v", err), http.StatusInternalServerError)
}
return c.NoContent(http.StatusOK)
}
func storeChunkInDB(ctx context.Context, uploadId string, chunkIndex int, chunkData []byte) error {
_, err := db.ExecContext(ctx, "INSERT INTO chunks (upload_id, chunk_index, chunk_data, created_at) VALUES ($1, $2, $3, NOW())", uploadId, chunkIndex, chunkData)
return err
}
func handleUploadComplete(c echo.Context) error {
uploadId := c.FormValue("uploadId")
chunkCount, err := strconv.Atoi(c.FormValue("chunkCount"))
if err != nil {
return handleError(c, fmt.Errorf("invalid chunk count: %v", err), http.StatusBadRequest)
}
fileName := c.FormValue("fileName")
key, err := generateRandomKey()
if err != nil {
return handleError(c, fmt.Errorf("error generating encryption key: %v", err), http.StatusInternalServerError)
return c.JSON(http.StatusInternalServerError, map[string]string{"error": fmt.Sprintf("error generating encryption key: %v", err)})
}
file, handler, err := r.FormFile("file")
if err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": fmt.Sprintf("error getting form file: %v", err)})
}
defer file.Close()
id := generateID()
for i := 0; i < chunkCount; i++ {
chunkData, err := getChunkFromDB(c.Request().Context(), uploadId, i)
if err != nil {
return handleError(c, fmt.Errorf("error retrieving chunk data: %v", err), http.StatusInternalServerError)
}
encryptedData, err := encryptFile(bytes.NewReader(chunkData), key)
if err != nil {
return handleError(c, fmt.Errorf("error encrypting chunk: %v", err), http.StatusInternalServerError)
}
encryptedData, err := encryptFile(file, key)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": fmt.Sprintf("error encrypting file: %v", err)})
}
if err := storeChunkInFilesTable(c.Request().Context(), id, fileName, i, encryptedData); err != nil {
return handleError(c, fmt.Errorf("error storing chunk in database: %v", err), http.StatusInternalServerError)
}
if err := storeFileInDB(r.Context(), id, handler.Filename, encryptedData); err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": fmt.Sprintf("error storing file in database: %v", err)})
}
encodedKey := hex.EncodeToString(key)
response := struct {
type UploadResponse struct {
ID string `json:"id"`
Key string `json:"key"`
}{
}
response := UploadResponse{
ID: id,
Key: encodedKey,
}
@ -188,93 +131,55 @@ func handleUploadComplete(c echo.Context) error {
return c.JSON(http.StatusOK, response)
}
func storeChunkInFilesTable(ctx context.Context, id, fileName string, chunkIndex int, encryptedData []byte) error {
_, err := db.ExecContext(ctx, "INSERT INTO files (id, name, chunk_index, chunk_data, created_at) VALUES ($1, $2, $3, $4, NOW())", id, fileName, chunkIndex, encryptedData)
return err
}
func getChunkFromDB(ctx context.Context, uploadId string, chunkIndex int) ([]byte, error) {
var chunkData []byte
err := db.QueryRowContext(ctx, "SELECT chunk_data FROM chunks WHERE upload_id = $1 AND chunk_index = $2", uploadId, chunkIndex).Scan(&chunkData)
return chunkData, err
}
func handleDownload(c echo.Context) error {
id := c.Param("id")
keyHex := c.QueryParam("key")
key, err := hex.DecodeString(keyHex)
if err != nil {
return handleError(c, fmt.Errorf("invalid key: %v", err), http.StatusBadRequest)
return c.JSON(http.StatusBadRequest, map[string]string{"error": fmt.Sprintf("invalid key: %v", err)})
}
fileName, err := getFileNameFromDB(c.Request().Context(), id)
fileName, encryptedData, err := getFileFromDB(c.Request().Context(), id)
if err != nil {
return handleError(c, fmt.Errorf("error getting file name from database: %v", err), http.StatusInternalServerError)
return c.JSON(http.StatusInternalServerError, map[string]string{"error": fmt.Sprintf("error getting file from database: %v", err)})
}
c.Response().Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, fileName))
err = decryptAndStreamChunks(c.Response(), id, key)
err = decryptAndStreamFile(c.Response(), encryptedData, key)
if err != nil {
return handleError(c, fmt.Errorf("error decrypting and streaming file: %v", err), http.StatusInternalServerError)
return c.JSON(http.StatusInternalServerError, map[string]string{"error": fmt.Sprintf("error decrypting and streaming file: %v", err)})
}
return nil
}
func getFileNameFromDB(ctx context.Context, id string) (fileName string, err error) {
err = db.QueryRowContext(ctx, "SELECT name FROM files WHERE id = $1 LIMIT 1", id).Scan(&fileName)
if err == sql.ErrNoRows {
return "", errors.New("file not found")
}
return fileName, err
}
func decryptAndStreamChunks(w io.Writer, id string, key []byte) error {
rows, err := db.Query("SELECT chunk_data FROM files WHERE id = $1 ORDER BY chunk_index", id)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var encryptedData []byte
if err := rows.Scan(&encryptedData); err != nil {
return err
}
plaintext, err := decryptFile(encryptedData, key)
if err != nil {
return err
}
_, err = w.Write(plaintext)
if err != nil {
return err
}
}
return rows.Err()
}
func handleGetFileInfo(c echo.Context) error {
id := c.Param("id")
keyHex := c.QueryParam("key")
key, err := hex.DecodeString(keyHex)
if err != nil {
return handleError(c, fmt.Errorf("invalid key: %v", err), http.StatusBadRequest)
return c.JSON(http.StatusBadRequest, map[string]string{"error": fmt.Sprintf("invalid key: %v", err)})
}
fileName, err := getFileNameFromDB(c.Request().Context(), id)
fileName, encryptedData, err := getFileFromDB(c.Request().Context(), id)
if err != nil {
return handleError(c, fmt.Errorf("error getting file name from database: %v", err), http.StatusInternalServerError)
return c.JSON(http.StatusInternalServerError, map[string]string{"error": fmt.Sprintf("error getting file from database: %v", err)})
}
fileSize, err := getTotalFileSize(id, key)
plaintext, err := decryptFile(encryptedData, key)
if err != nil {
return handleError(c, fmt.Errorf("error getting file size: %v", err), http.StatusInternalServerError)
return c.JSON(http.StatusInternalServerError, map[string]string{"error": fmt.Sprintf("error decrypting file: %v", err)})
}
fileSizeBytes := len(plaintext)
var fileSize string
if fileSizeBytes >= 1024*1024 {
fileSize = fmt.Sprintf("%.2f MB", float64(fileSizeBytes)/(1024*1024))
} else {
fileSize = fmt.Sprintf("%.2f KB", float64(fileSizeBytes)/1024)
}
fileInfo := struct {
@ -288,41 +193,100 @@ func handleGetFileInfo(c echo.Context) error {
return c.JSON(http.StatusOK, fileInfo)
}
func getTotalFileSize(id string, key []byte) (string, error) {
var totalSize int64
rows, err := db.Query("SELECT chunk_data FROM files WHERE id = $1 ORDER BY chunk_index", id)
func storeFileInDB(ctx context.Context, id, fileName string, encryptedData []byte) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return "", err
return err
}
defer rows.Close()
defer tx.Rollback()
for rows.Next() {
var encryptedData []byte
if err := rows.Scan(&encryptedData); err != nil {
return "", err
}
plaintext, err := decryptFile(encryptedData, key)
if err != nil {
return "", err
}
totalSize += int64(len(plaintext))
_, err = tx.ExecContext(ctx, "INSERT INTO files (id, name, data) VALUES ($1, $2, $3)", id, fileName, encryptedData)
if err != nil {
return err
}
var fileSize string
if totalSize >= 1024*1024 {
fileSize = fmt.Sprintf("%.2f MB", float64(totalSize)/(1024*1024))
} else {
fileSize = fmt.Sprintf("%.2f KB", float64(totalSize)/1024)
}
return fileSize, rows.Err()
return tx.Commit()
}
func handleError(c echo.Context, err error, status int) error {
fmt.Printf("error: %v\n", err)
return c.JSON(status, map[string]string{"error": err.Error()})
func getFileFromDB(ctx context.Context, id string) (fileName string, encryptedData []byte, err error) {
err = db.QueryRowContext(ctx, "SELECT name, data FROM files WHERE id = $1", id).Scan(&fileName, &encryptedData)
if err == sql.ErrNoRows {
return "", nil, errors.New("file not found")
}
return fileName, encryptedData, err
}
func handleError(c echo.Context, err error, code int) error {
return c.JSON(code, map[string]string{"error": err.Error()})
}
func encryptFile(in io.Reader, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, nonceSize)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
plaintext, err := io.ReadAll(in)
if err != nil {
return nil, err
}
ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil)
return append(nonce, ciphertext...), nil
}
func decryptAndStreamFile(w io.Writer, encryptedData []byte, key []byte) error {
if len(encryptedData) < nonceSize {
return errors.New("ciphertext too short")
}
block, err := aes.NewCipher(key)
if err != nil {
return err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return err
}
nonce, ciphertext := encryptedData[:nonceSize], encryptedData[nonceSize:]
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return err
}
_, err = w.Write(plaintext)
return err
}
func decryptFile(encryptedData []byte, key []byte) ([]byte, error) {
if len(encryptedData) < nonceSize {
return nil, errors.New("ciphertext too short")
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce, ciphertext := encryptedData[:nonceSize], encryptedData[nonceSize:]
return aesgcm.Open(nil, nonce, ciphertext, nil)
}
func generateRandomKey() ([]byte, error) {
@ -333,86 +297,8 @@ func generateRandomKey() ([]byte, error) {
func generateID() string {
b := make([]byte, 16)
rand.Read(b)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return hex.EncodeToString(b)
}
func encryptFile(plaintext io.Reader, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
nonce := make([]byte, nonceSize)
if _, err := rand.Read(nonce); err != nil {
return nil, err
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
plaintextBytes, err := io.ReadAll(plaintext)
if err != nil {
return nil, err
}
ciphertext := aesgcm.Seal(nonce, nonce, plaintextBytes, nil)
return ciphertext, nil
}
func decryptFile(ciphertext, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(ciphertext) < nonceSize {
return nil, errors.New("ciphertext too short")
}
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}
func startCleanupScheduler() {
ticker := time.NewTicker(24 * time.Hour)
go func() {
for range ticker.C {
cleanupChunks()
cleanupOldFiles()
}
}()
}
func cleanupChunks() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
_, err := db.ExecContext(ctx, "DELETE FROM chunks WHERE created_at < NOW() - INTERVAL '1 day'")
if err != nil {
fmt.Printf("error cleaning up chunks: %v\n", err)
}
}
func cleanupOldFiles() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
_, err := db.ExecContext(ctx, "DELETE FROM files WHERE created_at < NOW() - INTERVAL '1 day'")
if err != nil {
fmt.Printf("error cleaning up old files: %v\n", err)
}
}