Compare commits

...

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

11 changed files with 137 additions and 716 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>

View file

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

View file

@ -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)
}
}