package main import ( "os" "time" "gorm.io/gorm" ) // recoverMissingLocalUpload re-queues a task when its shared media file has // disappeared. This can happen after an older version deleted a shared file // after another user's upload. The asset is invalidated so the next worker // downloads it again instead of retrying an impossible WebDAV upload forever. func (a *App) recoverMissingLocalUpload(t *Task) bool { if t.AssetID == 0 || t.LocalPath == "" { return false } if _, err := os.Stat(t.LocalPath); err == nil || !os.IsNotExist(err) { return false } var queued bool err := a.transactionRetry(func(tx *gorm.DB) error { var asset MediaAsset if err := tx.First(&asset, t.AssetID).Error; err != nil { return err } if asset.Status == "done" && asset.LocalPath != "" { if _, statErr := os.Stat(asset.LocalPath); statErr == nil { if err := tx.Model(&Task{}).Where("id = ?", t.ID).Updates(map[string]any{ "local_path": asset.LocalPath, "format": asset.Format, "actual_quality": asset.ActualQuality, "status": "upload_pending", "error": "共享资源已恢复,正在继续上传", }).Error; err != nil { return err } queued = true return nil } } if asset.LocalPath != "" && asset.LocalPath != t.LocalPath { return nil } asset.Status = "error" asset.OwnerTaskID = 0 asset.LeaseUntil = time.Time{} asset.LocalPath = "" asset.Size = 0 if err := tx.Save(&asset).Error; err != nil { return err } if err := tx.Model(&Task{}).Where("id = ?", t.ID).Updates(map[string]any{ "status": "pending", "error": "本地共享文件不存在,正在重新下载", "progress": 0, "downloaded_bytes": 0, }).Error; err != nil { return err } queued = true return nil }) if err != nil || !queued { return false } a.enqueueTask(t.ID) return true } // resyncPendingUploads repairs tasks that finished locally while WebDAV was // unavailable or the user's WebDAV permission was not yet configured. func (a *App) resyncPendingUploads() { var tasks []Task if err := a.db.Where("local_path <> ? AND ((status IN ? AND (remote_path = '' OR remote_path IS NULL)) OR status = ?)", "", []string{"done", "upload_pending"}, "error").Order("id asc").Limit(100).Find(&tasks).Error; err != nil { return } for i := range tasks { t := tasks[i] if t.Status == "error" { status, err := a.taskReservationStatus(t.ID, t.CreditReservationID) if err != nil { continue } if status == "committed" { if err := a.repairCommittedTask(&t); err != nil { continue } } else { var asset MediaAsset if t.AssetID == 0 || a.db.First(&asset, t.AssetID).Error != nil || asset.Status != "done" || asset.LocalPath == "" { continue } if _, statErr := os.Stat(asset.LocalPath); statErr != nil { continue } t.LocalPath = asset.LocalPath t.Format = asset.Format t.ActualQuality = asset.ActualQuality t.Status = "upload_pending" t.Error = "共享资源已完成,历史失败任务恢复上传" if err := a.db.Model(&t).Updates(map[string]any{ "local_path": t.LocalPath, "format": t.Format, "actual_quality": t.ActualQuality, "status": t.Status, "error": t.Error, }).Error; err != nil { continue } } } if _, _, ok := a.storageForTask(&t); !ok { continue } if a.recoverMissingLocalUpload(&t) { continue } var current Task if a.db.First(¤t, t.ID).Error != nil { continue } if current.Status == "done" || current.Status == "upload_pending" { a.enqueueTask(current.ID) } } } // startWebDAVResync runs a small periodic repair loop. It is intentionally // conservative: only tasks with a completed local file and no confirmed remote path are retried. func (a *App) startWebDAVResync() { go func() { a.resyncPendingUploads() ticker := time.NewTicker(5 * time.Minute) defer ticker.Stop() for range ticker.C { a.resyncPendingUploads() } }() }