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