Compare commits
No commits in common. "main" and "main" have entirely different histories.
11 changed files with 137 additions and 716 deletions
11
Makefile
11
Makefile
|
@ -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
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"semi": false,
|
||||
"trailingComma": "none",
|
||||
"singleQuote": true,
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"bracketSpacing": true
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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)
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
|
@ -1,4 +1,4 @@
|
|||
module fileshare
|
||||
module file-share
|
||||
|
||||
go 1.22.2
|
||||
|
||||
|
|
399
server/main.go
399
server/main.go
|
@ -1,21 +1,16 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"embed"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
@ -24,37 +19,23 @@ 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))))
|
||||
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
|
||||
Root: "dist",
|
||||
Index: "index.html",
|
||||
HTML5: true,
|
||||
Filesystem: http.FS(dist),
|
||||
}))
|
||||
func RegisterHandlers(e *echo.Echo) {
|
||||
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 +44,12 @@ func main() {
|
|||
defer db.Close()
|
||||
|
||||
e := echo.New()
|
||||
registerHandlers(e)
|
||||
|
||||
startCleanupScheduler()
|
||||
|
||||
e.Logger.Fatal(e.Start(":" + port))
|
||||
RegisterHandlers(e)
|
||||
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://file:password@localhost/filedb?sslmode=disable")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -84,103 +57,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 handleError(c, fmt.Errorf("error parsing multipart form: %v", err), http.StatusBadRequest)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
file, handler, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
return handleError(c, fmt.Errorf("error getting form file: %v", err), http.StatusBadRequest)
|
||||
}
|
||||
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 handleError(c, fmt.Errorf("error encrypting file: %v", err), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
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 handleError(c, fmt.Errorf("error storing file in database: %v", err), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
encodedKey := hex.EncodeToString(key)
|
||||
response := struct {
|
||||
|
||||
type UploadResponse struct {
|
||||
ID string `json:"id"`
|
||||
Key string `json:"key"`
|
||||
}{
|
||||
}
|
||||
|
||||
response := UploadResponse{
|
||||
ID: id,
|
||||
Key: encodedKey,
|
||||
}
|
||||
|
@ -188,17 +118,6 @@ 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")
|
||||
|
@ -208,14 +127,14 @@ func handleDownload(c echo.Context) error {
|
|||
return handleError(c, fmt.Errorf("invalid key: %v", err), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
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 handleError(c, fmt.Errorf("error getting file from database: %v", err), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
@ -223,41 +142,6 @@ func handleDownload(c echo.Context) error {
|
|||
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")
|
||||
|
@ -267,14 +151,22 @@ func handleGetFileInfo(c echo.Context) error {
|
|||
return handleError(c, fmt.Errorf("invalid key: %v", err), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
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 handleError(c, fmt.Errorf("error getting file from database: %v", err), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
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 handleError(c, fmt.Errorf("error decrypting file: %v", err), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
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 +180,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 +284,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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue