{"record":{"id":"6789ab131d551bbe","repo":"Tencent/WeKnora","slug":"local-file-service-cannot-copy-q-w","errorCode":null,"errorMessage":"local file service cannot copy %q: %w","messagePattern":"local file service cannot copy %q: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/application/service/file/local.go","lineNumber":168,"sourceCode":"\t\treturn fmt.Errorf(\"failed to delete file: %w\", err)\n\t}\n\n\tlogger.Info(ctx, \"File deleted successfully\")\n\treturn nil\n}\n\n// CopyFile copies an existing local object to a new knowledge-owned object.\n// The destination uses the same layout as SaveFile (baseDir/{tenantID}/{knowledgeID}/{unique}{ext}),\n// and the copy is a real byte-for-byte copy (no hardlink) so deleting the source\n// never affects it. Returns ErrCrossBackendCopy when srcPath is not a local path.\nfunc (s *localFileService) CopyFile(ctx context.Context,\n\tsrcPath string, tenantID uint64, knowledgeID string,\n) (string, error) {\n\t// Only local paths are accepted. A provider scheme other than local://\n\t// (e.g. s3://, minio://) means a cross-backend copy, which this service\n\t// does not support. Legacy bare/absolute paths have no scheme and pass.\n\tif i := strings.Index(srcPath, \"://\"); i >= 0 && srcPath[:i+3] != localScheme {\n\t\treturn \"\", fmt.Errorf(\"local file service cannot copy %q: %w\", srcPath, ErrCrossBackendCopy)\n\t}\n\n\t// Validate and resolve the source path under baseDir (same guard as GetFile).\n\tsrcCandidate := s.normalizePathForBase(srcPath)\n\tsrcResolved, err := secutils.SafePathUnderBase(s.baseDir, srcCandidate)\n\tif err != nil {\n\t\tlogger.Errorf(ctx, \"Path traversal denied for CopyFile src: %v\", err)\n\t\treturn \"\", fmt.Errorf(\"invalid source path: %w\", err)\n\t}\n\n\t// Build destination path with the knowledge-owned layout.\n\tdir := filepath.Join(s.baseDir, fmt.Sprintf(\"%d\", tenantID), knowledgeID)\n\tif _, err := secutils.SafePathUnderBase(s.baseDir, dir); err != nil {\n\t\tlogger.Errorf(ctx, \"Path traversal denied for CopyFile dir: %v\", err)\n\t\treturn \"\", fmt.Errorf(\"invalid path: %w\", err)\n\t}\n\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create directory: %w\", err)","sourceCodeStart":150,"sourceCodeEnd":186,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/application/service/file/local.go#L150-L186","documentation":"CopyFile refuses to copy a source path whose provider scheme is not local:// (e.g. s3://, minio://). The local file service only handles local storage; a cross-backend copy would require downloading and re-uploading through another provider, which it does not implement. The sentinel ErrCrossBackendCopy is wrapped so callers can detect this case with errors.Is.","triggerScenarios":"Calling CopyFile with srcPath containing a non-local scheme (strings.Index finds \"://\" and the prefix is not the local scheme) — typically when a knowledge record's storage path points at an S3/MinIO object while the local service was selected.","commonSituations":"Migration from S3 to local storage or vice versa; tenant records created under a different storage backend config; misconfigured STORAGE_PROVIDER defaulting to local while data lives in S3.","solutions":["Route the copy to the service matching the source path's scheme (detect via the prefix before \"://\")","Fix the storage provider configuration so records and service agree (STORAGE_PROVIDER / backend selection)","Download from the source backend and re-upload via SaveFile if a manual cross-backend copy is truly needed","Check errors.Is(err, file.ErrCrossBackendCopy) in the caller to handle this case explicitly"],"exampleFix":"// before: wrong service for the scheme\ncopied, err := localSvc.CopyFile(ctx, record.StoragePath, tenantID, knowledgeID) // s3://...\n// after: dispatch by scheme\nvar svc file.Service = localSvc\nif strings.HasPrefix(record.StoragePath, \"s3://\") {\n\tsvc = s3Svc\n}\ncopied, err := svc.CopyFile(ctx, record.StoragePath, tenantID, knowledgeID)","handlingStrategy":"type-guard","validationCode":"func isLocalScheme(p string) bool {\n\ti := strings.Index(p, \"://\")\n\treturn i < 0 || p[:i+3] == \"local://\"\n}","typeGuard":"func isLocalScheme(p string) bool {\n\ti := strings.Index(p, \"://\")\n\treturn i < 0 || p[:i+3] == \"local://\"\n}","tryCatchPattern":"copied, err := svc.CopyFile(ctx, src, tenantID, knowledgeID)\nif errors.Is(err, file.ErrCrossBackendCopy) {\n\t// dispatch to the backend matching the scheme, or download+re-upload\n}","preventionTips":["Detect the source scheme before choosing the copy service","Keep STORAGE_PROVIDER config consistent with where records were written","Handle the ErrCrossBackendCopy sentinel explicitly in copy workflows","For migrations, implement an explicit download-then-SaveFile path"],"tags":["go","copy","storage-backend","unsupported-operation"],"backgroundTag":"cross-backend-copy-unsupported","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}