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