221 lines
5.6 KiB
Go
221 lines
5.6 KiB
Go
package boxstore
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"mime"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"warpbox/lib/helpers"
|
|
"warpbox/lib/models"
|
|
)
|
|
|
|
var manifestMu sync.Mutex
|
|
|
|
func CreateManifest(boxID string, request models.CreateBoxRequest) ([]models.BoxFile, error) {
|
|
retention := normalizeRetentionOption(request.RetentionKey)
|
|
usedNames := make(map[string]int, len(request.Files))
|
|
files := make([]models.BoxFile, 0, len(request.Files))
|
|
|
|
for _, fileRequest := range request.Files {
|
|
filename, ok := helpers.SafeFilename(fileRequest.Name)
|
|
if !ok {
|
|
return nil, fmt.Errorf("Invalid filename")
|
|
}
|
|
|
|
filename = helpers.UniqueNameInBatch(filename, usedNames)
|
|
fileID, err := helpers.RandomHexID(8)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Could not create file id")
|
|
}
|
|
|
|
mimeType := mime.TypeByExtension(strings.ToLower(filepath.Ext(filename)))
|
|
if mimeType == "" {
|
|
mimeType = "application/octet-stream"
|
|
}
|
|
|
|
files = append(files, models.BoxFile{
|
|
ID: fileID,
|
|
Name: filename,
|
|
Size: fileRequest.Size,
|
|
MimeType: mimeType,
|
|
Status: models.FileStatusWait,
|
|
})
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
disableZip := false
|
|
if request.AllowZip != nil {
|
|
disableZip = !*request.AllowZip
|
|
}
|
|
oneTimeDownload := retention.Key == OneTimeDownloadRetentionKey
|
|
if oneTimeDownload {
|
|
disableZip = false
|
|
}
|
|
|
|
manifest := models.BoxManifest{
|
|
Files: files,
|
|
CreatedAt: now,
|
|
RetentionKey: retention.Key,
|
|
RetentionLabel: retention.Label,
|
|
RetentionSecs: retention.Seconds,
|
|
DisableZip: disableZip,
|
|
OneTimeDownload: oneTimeDownload,
|
|
}
|
|
|
|
if password := strings.TrimSpace(request.Password); password != "" {
|
|
authToken, err := helpers.RandomHexID(16)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Could not secure upload box")
|
|
}
|
|
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Could not secure upload box")
|
|
}
|
|
|
|
manifest.PasswordHash = string(passwordHash)
|
|
manifest.PasswordHashAlg = "bcrypt"
|
|
manifest.AuthToken = authToken
|
|
}
|
|
|
|
if err := WriteManifest(boxID, manifest); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
decoratedFiles := make([]models.BoxFile, 0, len(files))
|
|
for _, file := range files {
|
|
decoratedFiles = append(decoratedFiles, DecorateFile(boxID, file))
|
|
}
|
|
|
|
return decoratedFiles, nil
|
|
}
|
|
func MarkFileStatus(boxID string, fileID string, status string) (models.BoxFile, error) {
|
|
if status != models.FileStatusWait && status != models.FileStatusWork && status != models.FileStatusReady && status != models.FileStatusFailed {
|
|
return models.BoxFile{}, fmt.Errorf("Invalid file status")
|
|
}
|
|
|
|
manifestMu.Lock()
|
|
defer manifestMu.Unlock()
|
|
|
|
manifest, err := readManifestUnlocked(boxID)
|
|
if err != nil {
|
|
return models.BoxFile{}, err
|
|
}
|
|
|
|
for index, file := range manifest.Files {
|
|
if file.ID != fileID {
|
|
continue
|
|
}
|
|
|
|
manifest.Files[index].Status = status
|
|
startRetentionIfTerminalUnlocked(&manifest)
|
|
if err := writeManifestUnlocked(boxID, manifest); err != nil {
|
|
return models.BoxFile{}, err
|
|
}
|
|
|
|
return DecorateFile(boxID, manifest.Files[index]), nil
|
|
}
|
|
|
|
return models.BoxFile{}, fmt.Errorf("File not found")
|
|
}
|
|
|
|
func ReadManifest(boxID string) (models.BoxManifest, error) {
|
|
manifestMu.Lock()
|
|
defer manifestMu.Unlock()
|
|
|
|
return readManifestUnlocked(boxID)
|
|
}
|
|
|
|
func WriteManifest(boxID string, manifest models.BoxManifest) error {
|
|
manifestMu.Lock()
|
|
defer manifestMu.Unlock()
|
|
|
|
return writeManifestUnlocked(boxID, manifest)
|
|
}
|
|
|
|
func RenewManifest(boxID string, seconds int64) (models.BoxManifest, error) {
|
|
manifestMu.Lock()
|
|
defer manifestMu.Unlock()
|
|
|
|
manifest, err := readManifestUnlocked(boxID)
|
|
if err != nil {
|
|
return manifest, err
|
|
}
|
|
if seconds <= 0 || manifest.OneTimeDownload || manifest.ExpiresAt.IsZero() {
|
|
return manifest, nil
|
|
}
|
|
manifest.ExpiresAt = time.Now().UTC().Add(time.Duration(seconds) * time.Second)
|
|
return manifest, writeManifestUnlocked(boxID, manifest)
|
|
}
|
|
func reconcileManifest(boxID string) (models.BoxManifest, error) {
|
|
manifestMu.Lock()
|
|
defer manifestMu.Unlock()
|
|
|
|
manifest, err := readManifestUnlocked(boxID)
|
|
if err != nil {
|
|
return manifest, err
|
|
}
|
|
|
|
changed := false
|
|
for index, file := range manifest.Files {
|
|
path, ok := SafeBoxFilePath(boxID, file.Name)
|
|
if !ok || ensureRegularFile(path) != nil {
|
|
continue
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil || !info.Mode().IsRegular() {
|
|
continue
|
|
}
|
|
|
|
if file.Status == models.FileStatusReady && file.Size == info.Size() {
|
|
continue
|
|
}
|
|
|
|
// The manifest is the UI source of truth, but disk wins when an upload
|
|
// was saved and the final status write/response was interrupted.
|
|
manifest.Files[index].Size = info.Size()
|
|
manifest.Files[index].MimeType = helpers.MimeTypeForFile(path, file.Name)
|
|
manifest.Files[index].Status = models.FileStatusReady
|
|
changed = true
|
|
}
|
|
|
|
if changed {
|
|
startRetentionIfTerminalUnlocked(&manifest)
|
|
if err := writeManifestUnlocked(boxID, manifest); err != nil {
|
|
return manifest, err
|
|
}
|
|
}
|
|
|
|
return manifest, nil
|
|
}
|
|
func readManifestUnlocked(boxID string) (models.BoxManifest, error) {
|
|
var manifest models.BoxManifest
|
|
data, err := os.ReadFile(ManifestPath(boxID))
|
|
if err != nil {
|
|
return manifest, err
|
|
}
|
|
|
|
if err := json.Unmarshal(data, &manifest); err != nil {
|
|
return manifest, err
|
|
}
|
|
|
|
return manifest, nil
|
|
}
|
|
|
|
// Manifest writes are serialized because the browser can upload several files
|
|
// concurrently into the same box. Without this lock, status updates can race.
|
|
func writeManifestUnlocked(boxID string, manifest models.BoxManifest) error {
|
|
data, err := json.MarshalIndent(manifest, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.WriteFile(ManifestPath(boxID), data, 0644)
|
|
}
|