360 lines
9.5 KiB
Go
360 lines
9.5 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"inou/lib"
|
|
)
|
|
|
|
// Upload represents a file upload entry for display
|
|
type Upload struct {
|
|
ID string
|
|
FileName, FilePath, SizeHuman, UploadedAt, ExpiresAt, DeletedReason string
|
|
Category, Status string
|
|
Deleted bool
|
|
}
|
|
|
|
// UploadData is the JSON structure stored in Entry.Data for uploads
|
|
type UploadData struct {
|
|
Path string `json:"path"`
|
|
Size int64 `json:"size"`
|
|
UploadedBy string `json:"uploaded_by"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
func formatBytes(b int64) string {
|
|
const unit = 1024
|
|
if b < unit {
|
|
return fmt.Sprintf("%d B", b)
|
|
}
|
|
div, exp := int64(unit), 0
|
|
for n := b / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp])
|
|
}
|
|
|
|
// getUploads returns all uploads for a dossier using lib.EntryList
|
|
func getUploads(dossierID string) []Upload {
|
|
var uploads []Upload
|
|
|
|
entries, err := lib.EntryList(nil, "", lib.CategoryUpload, &lib.EntryFilter{ // nil ctx - internal operation
|
|
DossierID: dossierID,
|
|
Limit: 50,
|
|
})
|
|
if err != nil {
|
|
return uploads
|
|
}
|
|
|
|
for _, e := range entries {
|
|
var data UploadData
|
|
json.Unmarshal([]byte(e.Data), &data)
|
|
|
|
u := Upload{
|
|
ID: e.EntryID,
|
|
FileName: e.Value,
|
|
FilePath: data.Path,
|
|
Category: e.Type,
|
|
Status: data.Status,
|
|
SizeHuman: formatBytes(data.Size),
|
|
UploadedAt: time.Unix(e.Timestamp, 0).Format("Jan 2"),
|
|
ExpiresAt: time.Unix(e.TimestampEnd, 0).Format("Jan 2"),
|
|
}
|
|
if e.Status != 0 {
|
|
u.Deleted = true
|
|
u.DeletedReason = "Deleted"
|
|
}
|
|
uploads = append(uploads, u)
|
|
}
|
|
return uploads
|
|
}
|
|
|
|
// getUploadEntry retrieves a single upload entry using lib.EntryGet
|
|
func getUploadEntry(entryID, dossierID string) (filePath, fileName, category, status string, deleted bool) {
|
|
e, err := lib.EntryGet(nil, entryID) // nil ctx - internal operation
|
|
if err != nil || e == nil {
|
|
return
|
|
}
|
|
|
|
// Verify it belongs to the right dossier and is an upload
|
|
if e.DossierID != dossierID || e.Category != lib.CategoryUpload {
|
|
return
|
|
}
|
|
|
|
var data UploadData
|
|
json.Unmarshal([]byte(e.Data), &data)
|
|
|
|
fileName = e.Value
|
|
category = e.Type
|
|
filePath = data.Path
|
|
status = data.Status
|
|
deleted = e.Status != 0
|
|
return
|
|
}
|
|
|
|
// findUploadByFilename finds existing uploads with the same filename
|
|
func findUploadByFilename(dossierID, filename string) []*lib.Entry {
|
|
entries, err := lib.EntryList(nil, "", lib.CategoryUpload, &lib.EntryFilter{ // nil ctx - internal operation
|
|
DossierID: dossierID,
|
|
Value: filename,
|
|
})
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return entries
|
|
}
|
|
|
|
func handleUploadPage(w http.ResponseWriter, r *http.Request) {
|
|
p := getLoggedInDossier(r)
|
|
if p == nil {
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
return
|
|
}
|
|
parts := strings.Split(r.URL.Path, "/")
|
|
if len(parts) < 4 {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
targetID := parts[2]
|
|
|
|
isSelf := targetID == p.DossierID
|
|
if !isSelf {
|
|
if access, found := getAccess(formatHexID(p.DossierID), formatHexID(targetID)); !found || !access.CanEdit {
|
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
|
|
target, _ := lib.DossierGet(nil, targetID) // nil ctx - internal operation
|
|
if target == nil {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
lang := getLang(r)
|
|
data := PageData{
|
|
Page: "upload", Lang: lang, T: translations[lang],
|
|
Dossier: p, TargetDossier: target,
|
|
UploadList: getUploads(targetID),
|
|
}
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
templates.ExecuteTemplate(w, "base.tmpl", data)
|
|
}
|
|
|
|
func handleUploadPost(w http.ResponseWriter, r *http.Request) {
|
|
p := getLoggedInDossier(r)
|
|
if p == nil {
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
parts := strings.Split(r.URL.Path, "/")
|
|
if len(parts) < 4 {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
targetID := parts[2]
|
|
|
|
isSelf := targetID == p.DossierID
|
|
if !isSelf {
|
|
if access, found := getAccess(formatHexID(p.DossierID), formatHexID(targetID)); !found || !access.CanEdit {
|
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
|
|
r.ParseMultipartForm(10 << 30)
|
|
file, header, err := r.FormFile("file")
|
|
if err != nil {
|
|
http.Error(w, "No file", http.StatusBadRequest)
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
relPath := r.FormValue("path")
|
|
if relPath == "" {
|
|
relPath = header.Filename
|
|
}
|
|
fileName := filepath.Base(relPath)
|
|
category := r.FormValue("category")
|
|
|
|
// Read file content
|
|
content, err := io.ReadAll(file)
|
|
if err != nil {
|
|
http.Error(w, "Failed to read", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Generate file ID using lib.NewID() (Hex16 format)
|
|
fileID := lib.NewID()
|
|
|
|
// Store in uploads directory
|
|
uploadDir := filepath.Join(uploadsDir, formatHexID(targetID))
|
|
filePath := filepath.Join(uploadDir, fileID)
|
|
os.MkdirAll(uploadDir, 0755)
|
|
if err := lib.EncryptFile(content, filePath); err != nil {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte(`{"status":"error","message":"Upload failed, we've been notified"}`))
|
|
return
|
|
}
|
|
written := int64(len(content))
|
|
|
|
// Delete existing upload with same filename (re-upload cleanup)
|
|
existingUploads := findUploadByFilename(targetID, fileName)
|
|
for _, old := range existingUploads {
|
|
lib.EntryDeleteTree(targetID, old.EntryID)
|
|
}
|
|
|
|
now := time.Now().Unix()
|
|
expires := now + 7*24*60*60
|
|
|
|
uploadData := UploadData{
|
|
Path: filePath,
|
|
Size: written,
|
|
UploadedBy: p.DossierID,
|
|
Status: "uploaded",
|
|
}
|
|
dataJSON, _ := json.Marshal(uploadData)
|
|
|
|
uploadEntry := &lib.Entry{
|
|
DossierID: targetID,
|
|
Category: lib.CategoryUpload,
|
|
Type: category,
|
|
Value: fileName,
|
|
Timestamp: now,
|
|
TimestampEnd: expires,
|
|
Data: string(dataJSON),
|
|
}
|
|
lib.EntryWrite(nil, uploadEntry) // nil ctx - internal operation
|
|
entryID := uploadEntry.EntryID
|
|
lib.AuditLog(p.DossierID, "file_upload", targetID, fileName)
|
|
|
|
// Notify via Signal
|
|
if target, _ := lib.DossierGet(nil, targetID); target != nil { // nil ctx - internal operation
|
|
sizeStr := fmt.Sprintf("%d B", written)
|
|
if written >= 1024*1024 {
|
|
sizeStr = fmt.Sprintf("%.1f MB", float64(written)/(1024*1024))
|
|
} else if written >= 1024 {
|
|
sizeStr = fmt.Sprintf("%.1f KB", float64(written)/1024)
|
|
}
|
|
lib.SendSignal(fmt.Sprintf("Upload: %s → %s (%s, %s, %s)", p.Name, target.Name, category, fileName, sizeStr))
|
|
}
|
|
|
|
// Spawn genome processing for genetics uploads
|
|
if category == "genetics" {
|
|
go processGenomeUpload(entryID, targetID, filePath)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(fmt.Sprintf(`{"status":"ok","id":"%s"}`, entryID)))
|
|
}
|
|
|
|
func handleDeleteFile(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
p := getLoggedInDossier(r)
|
|
if p == nil {
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Parse /dossier/{hex}/files/{id}/delete
|
|
parts := strings.Split(r.URL.Path, "/")
|
|
if len(parts) < 6 {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
targetID := parts[2]
|
|
fileID := parts[4]
|
|
|
|
isSelf := targetID == p.DossierID
|
|
if !isSelf {
|
|
if access, found := getAccess(formatHexID(p.DossierID), formatHexID(targetID)); !found || !access.CanEdit {
|
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Get file info for audit and deletion
|
|
filePath, fileName, _, _, _ := getUploadEntry(fileID, targetID)
|
|
if filePath != "" {
|
|
os.Remove(filePath)
|
|
}
|
|
|
|
// Mark as deleted using lib.EntryGet + lib.EntryWrite
|
|
entry, err := lib.EntryGet(nil, fileID) // nil ctx - internal operation
|
|
if err == nil && entry != nil && entry.DossierID == targetID {
|
|
entry.Status = 1 // Mark as deleted
|
|
lib.EntryWrite(nil, entry) // nil ctx - internal operation
|
|
}
|
|
|
|
lib.AuditLog(p.DossierID, "file_delete", targetID, fileName)
|
|
http.Redirect(w, r, fmt.Sprintf("/dossier/%s/upload", formatHexID(targetID)), http.StatusSeeOther)
|
|
}
|
|
|
|
func handleUpdateFile(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
p := getLoggedInDossier(r)
|
|
if p == nil {
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Parse /dossier/{hex}/files/{id}/update
|
|
parts := strings.Split(r.URL.Path, "/")
|
|
if len(parts) < 6 {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
targetID := parts[2]
|
|
fileID := parts[4]
|
|
|
|
isSelf := targetID == p.DossierID
|
|
if !isSelf {
|
|
if access, found := getAccess(formatHexID(p.DossierID), formatHexID(targetID)); !found || !access.CanEdit {
|
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Get entry using lib.EntryGet
|
|
entry, err := lib.EntryGet(nil, fileID) // nil ctx - internal operation
|
|
if err != nil || entry == nil || entry.DossierID != targetID || entry.Category != lib.CategoryUpload {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
if entry.Status != 0 {
|
|
http.NotFound(w, r) // Deleted
|
|
return
|
|
}
|
|
|
|
var data UploadData
|
|
json.Unmarshal([]byte(entry.Data), &data)
|
|
|
|
if data.Status != "uploaded" {
|
|
http.Error(w, "Cannot modify processed file", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
newCategory := r.FormValue("category")
|
|
if newCategory != "" && newCategory != entry.Type {
|
|
entry.Type = newCategory
|
|
lib.EntryWrite(nil, entry) // nil ctx - internal operation
|
|
lib.AuditLog(p.DossierID, "file_category_change", targetID, entry.Value)
|
|
}
|
|
|
|
http.Redirect(w, r, fmt.Sprintf("/dossier/%s/upload", formatHexID(targetID)), http.StatusSeeOther)
|
|
}
|