Initial commit
This commit is contained in:
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