Compare commits

..

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

11 changed files with 716 additions and 137 deletions

11
Makefile Normal file
View file

@ -0,0 +1,11 @@
.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

9
client/.prettierrc Normal file
View file

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

22
client/assets/fonts.css Normal file
View file

@ -0,0 +1,22 @@
/* 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.

183
client/assets/index.js Normal file
View file

@ -0,0 +1,183 @@
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)
}

179
client/assets/styles.css Normal file
View file

@ -0,0 +1,179 @@
@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;
}
}

48
client/index.html Normal file
View file

@ -0,0 +1,48 @@
<!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>

View file

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

View file

@ -1,16 +1,21 @@
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"
@ -19,23 +24,37 @@ import (
)
const (
maxUploadSize = 10 * 1024 * 1024 // 10 MB
maxUploadSize = 3 * 1024 * 1024 * 1024 // 3 GB
keySize = 32
nonceSize = 12
)
var db *sql.DB
var port string
func RegisterHandlers(e *echo.Echo) {
//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),
}))
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())
e.POST("/upload", handleUpload)
e.POST("/upload_chunk", handleUploadChunk)
e.POST("/upload_complete", handleUploadComplete)
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 {
@ -44,12 +63,20 @@ func main() {
defer db.Close()
e := echo.New()
RegisterHandlers(e)
e.Logger.Fatal(e.Start(":8080"))
registerHandlers(e)
startCleanupScheduler()
e.Logger.Fatal(e.Start(":" + port))
}
func initDB() (*sql.DB, error) {
db, err := sql.Open("postgres", "postgres://file:password@localhost/filedb?sslmode=disable")
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)
if err != nil {
return nil, err
}
@ -57,60 +84,103 @@ func initDB() (*sql.DB, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := createFilesTable(ctx, db); err != nil {
if err := createTables(ctx, db); err != nil {
return nil, err
}
return db, nil
}
func createFilesTable(ctx context.Context, db *sql.DB) error {
func createTables(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 PRIMARY KEY,
id TEXT,
name TEXT,
data BYTEA
chunk_index INT,
chunk_data BYTEA,
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (id, chunk_index)
);
`)
return err
}
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)
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)
}
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(file, key)
if err != nil {
return handleError(c, fmt.Errorf("error encrypting file: %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)
}
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)
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)
}
}
encodedKey := hex.EncodeToString(key)
type UploadResponse struct {
response := struct {
ID string `json:"id"`
Key string `json:"key"`
}
response := UploadResponse{
}{
ID: id,
Key: encodedKey,
}
@ -118,6 +188,17 @@ func handleUpload(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")
@ -127,14 +208,14 @@ func handleDownload(c echo.Context) error {
return handleError(c, fmt.Errorf("invalid key: %v", err), http.StatusBadRequest)
}
fileName, encryptedData, err := getFileFromDB(c.Request().Context(), id)
fileName, err := getFileNameFromDB(c.Request().Context(), id)
if err != nil {
return handleError(c, fmt.Errorf("error getting file from database: %v", err), http.StatusInternalServerError)
return handleError(c, fmt.Errorf("error getting file name from database: %v", err), http.StatusInternalServerError)
}
c.Response().Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, fileName))
err = decryptAndStreamFile(c.Response(), encryptedData, key)
err = decryptAndStreamChunks(c.Response(), id, key)
if err != nil {
return handleError(c, fmt.Errorf("error decrypting and streaming file: %v", err), http.StatusInternalServerError)
}
@ -142,6 +223,41 @@ 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")
@ -151,22 +267,14 @@ func handleGetFileInfo(c echo.Context) error {
return handleError(c, fmt.Errorf("invalid key: %v", err), http.StatusBadRequest)
}
fileName, encryptedData, err := getFileFromDB(c.Request().Context(), id)
fileName, err := getFileNameFromDB(c.Request().Context(), id)
if err != nil {
return handleError(c, fmt.Errorf("error getting file from database: %v", err), http.StatusInternalServerError)
return handleError(c, fmt.Errorf("error getting file name from database: %v", err), http.StatusInternalServerError)
}
plaintext, err := decryptFile(encryptedData, key)
fileSize, err := getTotalFileSize(id, key)
if err != nil {
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)
return handleError(c, fmt.Errorf("error getting file size: %v", err), http.StatusInternalServerError)
}
fileInfo := struct {
@ -180,100 +288,41 @@ func handleGetFileInfo(c echo.Context) error {
return c.JSON(http.StatusOK, fileInfo)
}
func storeFileInDB(ctx context.Context, id, fileName string, encryptedData []byte) error {
tx, err := db.BeginTx(ctx, nil)
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)
if err != nil {
return err
return "", err
}
defer tx.Rollback()
defer rows.Close()
_, err = tx.ExecContext(ctx, "INSERT INTO files (id, name, data) VALUES ($1, $2, $3)", id, fileName, encryptedData)
if err != nil {
return err
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))
}
return tx.Commit()
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()
}
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 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 generateRandomKey() ([]byte, error) {
@ -284,8 +333,86 @@ func generateRandomKey() ([]byte, error) {
func generateID() string {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
panic(err)
}
rand.Read(b)
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)
}
}