{"record":{"id":"2da7e60c045c86d7","repo":"flipped-aurora/gin-vue-admin","slug":"error-2da7e6","errorCode":null,"errorMessage":"文件删除失败: ","messagePattern":"文件删除失败: ","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/utils/upload/local.go","lineNumber":100,"sourceCode":"\nfunc (l *Local) DeleteFile(ctx context.Context, key string) error {\n\tp, err := l.localPath(ctx, key)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 检查文件是否存在\n\tif _, err := os.Stat(p); os.IsNotExist(err) {\n\t\treturn errors.New(\"文件不存在\")\n\t}\n\n\t// 使用文件锁防止并发删除\n\tmu.Lock()\n\tdefer mu.Unlock()\n\n\terr = os.Remove(p)\n\tif err != nil {\n\t\treturn errors.New(\"文件删除失败: \" + err.Error())\n\t}\n\n\treturn nil\n}\n\n// localPath 校验 key 并拼接出本地存储的绝对路径，复用 DeleteFile 中的路径穿越防护逻辑。\nfunc (*Local) localPath(ctx context.Context, key string) (string, error) {\n\t// 检查 key 是否为空\n\tif key == \"\" {\n\t\treturn \"\", errors.New(\"key不能为空\")\n\t}\n\n\t// 验证 key 是否包含非法字符或尝试访问存储路径之外的文件\n\tif strings.Contains(key, \"..\") || strings.ContainsAny(key, `\\/:*?\"<>|`) {\n\t\treturn \"\", errors.New(\"非法的key\")\n\t}\n\n\treturn filepath.Join(global.GVA_CONFIG.Local.StorePath, key), nil","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/flipped-aurora/gin-vue-admin/blob/3136500ef380842b0eb6c4daa87c3f8a47fcf9e0/server/utils/upload/local.go#L82-L118","documentation":"This error is returned by Local.DeleteFile when os.Remove(p) fails after the file was confirmed to exist and the delete lock was acquired. It wraps the underlying OS error (permission denied, device busy, read-only filesystem, etc.) appended after the '文件删除失败: ' prefix.","triggerScenarios":"DeleteFile called on an existing file whose removal fails: process lacks write permission on the containing directory, the file is open/locked by another process (Windows), the store lives on a read-only mount, or a race removed the file between Stat and Remove.","commonSituations":"Uploads directory owned by root while the server runs as a non-root user; Windows services holding files open (antivirus scanners, log shippers); Kubernetes volumes mounted read-only by mistake; NFS stale handles.","solutions":["Read the appended OS error and fix the cause: chmod/chown the StorePath directory so the process user has write+execute permission.","On Windows, identify and close processes holding the file open (handle.exe / Resource Monitor), or retry after they release it.","If storage is a mounted volume, verify it is mounted read-write (mount options, K8s volume spec).","If it's a Stat/Remove race, retry the delete once — the second attempt will report 文件不存在 which you can treat as success."],"exampleFix":"// before\n# ls -l uploads -> drwxr-xr-x root root; server runs as appuser\n// after\n# shell\nsudo chown appuser:appuser uploads && chmod 755 uploads","handlingStrategy":"try-catch","validationCode":"p := filepath.Join(storePath, key)\ninfo, err := os.Stat(p)\nif err != nil {\n    return fmt.Errorf(\"cannot access file: %w\", err)\n}\ndir := filepath.Dir(p)\nif ti, err := os.Stat(dir); err != nil || ti.Mode()&0o200 == 0 {\n    return errors.New(\"store directory is not writable by this process\")\n}","typeGuard":null,"tryCatchPattern":"err := local.DeleteFile(ctx, key)\nif err != nil {\n    if strings.HasPrefix(err.Error(), \"文件删除失败\") {\n        // inspect appended OS error; retry once on transient EBUSY, escalate on EACCES\n        time.Sleep(100 * time.Millisecond)\n        err = local.DeleteFile(ctx, key)\n    }\n    if err != nil {\n        return err\n    }\n}","preventionTips":["Grant the server process write+execute on the store directory (chown/chmod at deploy time).","Avoid read-only mounts for the uploads volume; assert writability in a readiness probe.","On Windows, exclude the uploads dir from scanners that hold files open.","Log the appended OS error verbatim to distinguish permission vs busy vs stale-handle."],"tags":["filesystem","file-deletion","permissions","go"],"backgroundTag":"file-delete-permission-denied","analyzedSha":"3136500ef380842b0eb6c4daa87c3f8a47fcf9e0","analyzedAt":"2026-08-31T13:50:02.721Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}