Initial commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
files
|
||||||
|
chspfiles.db
|
||||||
125
README.md
Normal file
125
README.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# Chookspace Files
|
||||||
|
|
||||||
|
A file management server over HTTP. Currently supports:
|
||||||
|
|
||||||
|
* Accounts with username and password
|
||||||
|
* Use a token to authenticate yourself
|
||||||
|
* Upload and download files
|
||||||
|
* You cannot access other people's files
|
||||||
|
|
||||||
|
## Setup (Server)
|
||||||
|
|
||||||
|
1. Install Go
|
||||||
|
2. cd to the server directory and run `go build`
|
||||||
|
3. Run the `files` binary
|
||||||
|
|
||||||
|
## Setup (Client)
|
||||||
|
|
||||||
|
Client coming soon!
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The `files` server binary takes three arguments:
|
||||||
|
|
||||||
|
* `--storage-path`: Tells Files where to keep files that the user has uploaded. Default is `/opt/chookspace-files`
|
||||||
|
* `--db-path`: Tells Files where to keep the database that tracks user information. Default is `/opt/chookspace-files/files.db`
|
||||||
|
* `--port`: Tells Files which port to start the server on. Default is `5001`
|
||||||
|
|
||||||
|
### HTTPS
|
||||||
|
|
||||||
|
To reduce the attack surface, Files does not directly support HTTP. We recommend using a reverse proxy such as Caddy or a tunnel system like Cloudflare to allow secure access to the server.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### /api/create_user
|
||||||
|
|
||||||
|
Creates a user in the database. Accepts JSON-formatted data.
|
||||||
|
|
||||||
|
Format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "YOUR_USERNAME",
|
||||||
|
"password": "YOUR_PASSWORD"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
May return the following status codes:
|
||||||
|
|
||||||
|
* 200: Everything went fine
|
||||||
|
* 400: Either:
|
||||||
|
* Bad JSON
|
||||||
|
* A user with this username already exists
|
||||||
|
* 500: Either:
|
||||||
|
* Error with BCrypt for password hashing
|
||||||
|
* SQL error
|
||||||
|
|
||||||
|
### /api/create_session
|
||||||
|
|
||||||
|
Creates a token which can be used to authenticate yourself when uploading and downloading files. Accepts JSON-formatted data.
|
||||||
|
|
||||||
|
Format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"username": "YOUR_USERNAME",
|
||||||
|
"password": "YOUR_PASSWORD"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
May return the following status codes:
|
||||||
|
|
||||||
|
* 200: Everything went fine. Check the JSON field "token" for your token.
|
||||||
|
* 400: Bad JSON
|
||||||
|
* 401: Either:
|
||||||
|
* User not found
|
||||||
|
* Incorrect password
|
||||||
|
* 500: SQL error
|
||||||
|
|
||||||
|
### /api/send_file
|
||||||
|
|
||||||
|
Sends a file to the server. Uses a token for authentication. Accepts a file and form data (files and JSON don't mix)
|
||||||
|
|
||||||
|
Format:
|
||||||
|
|
||||||
|
```
|
||||||
|
Form Data:
|
||||||
|
token: YOUR_TOKEN
|
||||||
|
path: WHERE_TO_STORE_FILE
|
||||||
|
```
|
||||||
|
|
||||||
|
May return the following status codes:
|
||||||
|
|
||||||
|
* 200: Everything went fine. Should return JSON with the SHA256 file hash and file size in bytes.
|
||||||
|
* 400: Either:
|
||||||
|
* Bad form data
|
||||||
|
* No file provided
|
||||||
|
* 401: Unknown token
|
||||||
|
* 500: Either:
|
||||||
|
* Failed to read file
|
||||||
|
* Failed to create storage directory
|
||||||
|
* Failed to save file
|
||||||
|
* Failed to save file metadata to database
|
||||||
|
|
||||||
|
### /api/get_file
|
||||||
|
|
||||||
|
Recieves a file from the server. Uses a token for authentication. Accepts JSON-formatted data.
|
||||||
|
|
||||||
|
Format:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "YOUR_TOKEN",
|
||||||
|
"path": "FILE_STORAGE_PATH"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
May return the following status codes:
|
||||||
|
|
||||||
|
* 200: Everything went fine. The content recieved should be the content of your file.
|
||||||
|
* 400: Bad JSON
|
||||||
|
* 401: Unknown token
|
||||||
|
* 404: File not found
|
||||||
|
* 500: Either:
|
||||||
|
* SQL error
|
||||||
|
* File found in DB but not on disk
|
||||||
20
server/go.mod
Normal file
20
server/go.mod
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
module chookspace/files
|
||||||
|
|
||||||
|
go 1.25.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
golang.org/x/crypto v0.46.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||||
|
golang.org/x/sys v0.39.0 // indirect
|
||||||
|
modernc.org/libc v1.66.10 // indirect
|
||||||
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
modernc.org/sqlite v1.43.0 // indirect
|
||||||
|
)
|
||||||
31
server/go.sum
Normal file
31
server/go.sum
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||||
|
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||||
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||||
|
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
||||||
|
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
||||||
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
|
modernc.org/sqlite v1.43.0 h1:8YqiFx3G1VhHTXO2Q00bl1Wz9KhS9Q5okwfp9Y97VnA=
|
||||||
|
modernc.org/sqlite v1.43.0/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8=
|
||||||
436
server/main.go
Normal file
436
server/main.go
Normal file
@@ -0,0 +1,436 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
ID uint64 `json:"id"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
ParentId uint64 `json:"parent_id"`
|
||||||
|
IsDirectory bool `json:"is_directory"`
|
||||||
|
|
||||||
|
Size uint64 `json:"size"`
|
||||||
|
MimeType string `json:"mime_type"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
|
||||||
|
OwnerId uint64 `json:"owner_id"`
|
||||||
|
|
||||||
|
UploadToken string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserFromToken(db *sql.DB, token string) (uint64, error) {
|
||||||
|
var userID uint64
|
||||||
|
var expiresAt time.Time
|
||||||
|
|
||||||
|
query := `SELECT user_id, expires_at FROM tokens WHERE token = ?`
|
||||||
|
err := db.QueryRow(query, token).Scan(&userID, &expiresAt)
|
||||||
|
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return 0, fmt.Errorf("invalid token")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().After(expiresAt) {
|
||||||
|
return 0, fmt.Errorf("token expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
return userID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
storagePath := flag.String("storage-path", "/opt/chsp_files", "Path where user uploaded files are stored.")
|
||||||
|
dbPath := flag.String("db-path", "/opt/chsp_files/files.db", "Path to the SQLite database which stores file information")
|
||||||
|
port := flag.Int("port", 5001, "Port to run the server on")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
log.SetPrefix("chookspace/files ")
|
||||||
|
|
||||||
|
// Set up database
|
||||||
|
db, err := sql.Open("sqlite", *dbPath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Ping(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Create tables
|
||||||
|
query := `
|
||||||
|
CREATE TABLE IF NOT EXISTS users(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT UNIQUE NOT NULL,
|
||||||
|
password TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tokens(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
token TEXT UNIQUE NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
expires_at DATETIME NOT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS files(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
path TEXT UNIQUE NOT NULL,
|
||||||
|
parent_id INTEGER,
|
||||||
|
is_directory BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
size INTEGER DEFAULT 0,
|
||||||
|
mime_type TEXT,
|
||||||
|
hash TEXT,
|
||||||
|
|
||||||
|
storage_path TEXT,
|
||||||
|
|
||||||
|
owner_id INTEGER NOT NULL,
|
||||||
|
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (parent_id) REFERENCES files(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`
|
||||||
|
if _, err := db.Exec(query); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprint(w, "dingus")
|
||||||
|
log.Print("we recieved a get to the root")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle user creation
|
||||||
|
http.HandleFunc("/api/create_user", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var user User
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// Decode JSON
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
fmt.Fprintf(w, `{
|
||||||
|
"status": "error",
|
||||||
|
"why": "%s"
|
||||||
|
}`, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
hash, hasherr := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
|
||||||
|
if hasherr != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
fmt.Fprintf(w, `{
|
||||||
|
"status": "error",
|
||||||
|
"why": "%s"
|
||||||
|
}`, hasherr.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do SQL stuff
|
||||||
|
_, dberr := db.Exec(`INSERT INTO users (username, password) VALUES (?, ?)`, user.Username, string(hash))
|
||||||
|
if dberr != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
fmt.Fprintf(w, `{
|
||||||
|
"status": "error",
|
||||||
|
"why": "%s"
|
||||||
|
}`, dberr.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yay everything worked it seems
|
||||||
|
fmt.Fprintf(w, `{
|
||||||
|
"status": "success"
|
||||||
|
}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle getting files
|
||||||
|
http.HandleFunc("/api/get_file", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Parse JSON
|
||||||
|
var file File
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&file); err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
fmt.Fprintf(w, `{
|
||||||
|
"status": "error",
|
||||||
|
"description": "bad JSON",
|
||||||
|
"why": "%s"
|
||||||
|
}`, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate user
|
||||||
|
uid, err := getUserFromToken(db, file.UploadToken)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
fmt.Fprintf(w, `{
|
||||||
|
"status": "error",
|
||||||
|
"description", "%s"
|
||||||
|
}`, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file.OwnerId = uid
|
||||||
|
|
||||||
|
// Find file
|
||||||
|
fileQuery := `SELECT storage_path, mime_type, size, name FROM files WHERE owner_id = ? AND path = ?`
|
||||||
|
if err := db.QueryRow(fileQuery, file.OwnerId, file.Path).Scan(&file.Path, &file.MimeType, &file.Size, &file.Name); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
fmt.Fprintf(w, `{
|
||||||
|
"status": "error",
|
||||||
|
"description": "file not found",
|
||||||
|
"why": "%s"
|
||||||
|
}`, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
fmt.Fprintf(w, `{
|
||||||
|
"status": "error",
|
||||||
|
"description": "internal server error",
|
||||||
|
"why": "%s"
|
||||||
|
}`, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the file
|
||||||
|
storageFile, err := os.Open(file.Path)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
fmt.Fprintf(w, `{
|
||||||
|
"status": "error",
|
||||||
|
"why": "%s"
|
||||||
|
}`, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer storageFile.Close()
|
||||||
|
|
||||||
|
// Set headers
|
||||||
|
w.Header().Set("Content-Type", file.MimeType)
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, file.Name))
|
||||||
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", file.Size))
|
||||||
|
if _, err := io.Copy(w, storageFile); err != nil {
|
||||||
|
log.Printf("Error while sending file: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle files sent to the server (special since JSON and files don't mix)
|
||||||
|
http.HandleFunc("/api/send_file", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// Parse multipart form (32MB max memory)
|
||||||
|
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"status": "error",
|
||||||
|
"why": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get auth credentials from form fields
|
||||||
|
token := r.FormValue("token")
|
||||||
|
|
||||||
|
// Authenticate user
|
||||||
|
uid, err := getUserFromToken(db, token)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
fmt.Fprintf(w, `{
|
||||||
|
"status": "error",
|
||||||
|
"description", "%s"
|
||||||
|
}`, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userID := uid
|
||||||
|
|
||||||
|
// Get the uploaded file
|
||||||
|
file, header, err := r.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"status": "error",
|
||||||
|
"why": "no file provided",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Read file contents and compute hash
|
||||||
|
fileBytes, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"status": "error",
|
||||||
|
"why": "failed to read file",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute SHA256 hash
|
||||||
|
hasher := sha256.New()
|
||||||
|
hasher.Write(fileBytes)
|
||||||
|
fileHash := hex.EncodeToString(hasher.Sum(nil))
|
||||||
|
|
||||||
|
// Create storage directory structure (first 2 chars of hash)
|
||||||
|
storageDir := filepath.Join(*storagePath, fileHash[:2], fileHash[2:4])
|
||||||
|
if err := os.MkdirAll(storageDir, 0755); err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"status": "error",
|
||||||
|
"why": "failed to create storage directory",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage path: storage/ab/cd/abcdef123456...
|
||||||
|
storagePath := filepath.Join(storageDir, fileHash)
|
||||||
|
|
||||||
|
// Write file to disk (only if it doesn't already exist - deduplication!)
|
||||||
|
if _, err := os.Stat(storagePath); os.IsNotExist(err) {
|
||||||
|
if err := os.WriteFile(storagePath, fileBytes, 0644); err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"status": "error",
|
||||||
|
"why": "failed to save file",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get file path from form (or default to root)
|
||||||
|
filePath := r.FormValue("path")
|
||||||
|
if filePath == "" {
|
||||||
|
filePath = "/" + header.Filename
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect MIME type
|
||||||
|
mimeType := http.DetectContentType(fileBytes)
|
||||||
|
|
||||||
|
// Insert into database
|
||||||
|
insertQuery := `
|
||||||
|
INSERT INTO files (name, path, is_directory, size, mime_type, hash, storage_path, owner_id)
|
||||||
|
VALUES (?, ?, 0, ?, ?, ?, ?, ?)
|
||||||
|
`
|
||||||
|
_, err = db.Exec(insertQuery,
|
||||||
|
header.Filename,
|
||||||
|
filePath,
|
||||||
|
len(fileBytes),
|
||||||
|
mimeType,
|
||||||
|
fileHash,
|
||||||
|
storagePath,
|
||||||
|
userID,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"status": "error",
|
||||||
|
"why": "failed to save metadata",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"status": "success",
|
||||||
|
"hash": fileHash,
|
||||||
|
"size": len(fileBytes),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
http.HandleFunc("/api/create_session", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var user User
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// Decode JSON
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
fmt.Fprintf(w, `{
|
||||||
|
"status": "error",
|
||||||
|
"description": "bad JSON",
|
||||||
|
"why": "%s"
|
||||||
|
}`, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate user
|
||||||
|
var hashedPassword string
|
||||||
|
authQuery := `SELECT id, password FROM users WHERE username = ?`
|
||||||
|
if err := db.QueryRow(authQuery, user.Username).Scan(&user.ID, &hashedPassword); err != nil {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
fmt.Fprintf(w, `{
|
||||||
|
"status": "error",
|
||||||
|
"description": "user not found",
|
||||||
|
"why": "%s"
|
||||||
|
}`, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(user.Password)); err != nil {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
fmt.Fprintf(w, `{
|
||||||
|
"status": "error",
|
||||||
|
"description": "incorrect password",
|
||||||
|
"why": "%s"
|
||||||
|
}`, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenBytes := make([]byte, 64)
|
||||||
|
rand.Read(tokenBytes)
|
||||||
|
token := hex.EncodeToString(tokenBytes)
|
||||||
|
expiresAt := time.Now().Add(30 * 24 * time.Hour)
|
||||||
|
|
||||||
|
_, err := db.Exec(`INSERT INTO tokens (user_id, token, expires_at) VALUES (?, ?, ?)`, user.ID, token, expiresAt)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
fmt.Fprintf(w, `{
|
||||||
|
"status": "error",
|
||||||
|
"description": "internal server error",
|
||||||
|
"why": "%s"
|
||||||
|
}`, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, `{
|
||||||
|
"status": "success",
|
||||||
|
"token": "%s"
|
||||||
|
}`, token)
|
||||||
|
})
|
||||||
|
|
||||||
|
http.ListenAndServe(strings.Join([]string{":", strconv.Itoa(*port)}, ""), nil)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user