{"record":{"id":"40da12871a29f493","repo":"Tencent/WeKnora","slug":"obs-copy-rejected-source-q-w","errorCode":null,"errorMessage":"obs copy rejected source %q: %w","messagePattern":"obs copy rejected source %q: %w","errorType":"validation","errorClass":"ErrCrossBackendCopy","httpStatus":null,"severity":"error","filePath":"internal/application/service/file/obs.go","lineNumber":253,"sourceCode":"\tif s.proxyDomain != \"\" {\n\t\treturn s.proxyDomain + \"/\" + strings.TrimPrefix(objectKey, \"/\"), nil\n\t}\n\n\treturn fmt.Sprintf(\"%s/%s/%s\", s.endpoint, s.bucketName, strings.TrimPrefix(objectKey, \"/\")), nil\n}\n\n// CopyFile copies an existing OBS object to a new knowledge-owned object using a\n// server-side CopyObject (OBS is S3-compatible). The destination uses the same\n// layout as SaveFile. Returns ErrCrossBackendCopy when srcPath does not belong\n// to this OBS service.\nfunc (s *obsFileService) CopyFile(ctx context.Context,\n\tsrcPath string, tenantID uint64, knowledgeID string,\n) (string, error) {\n\t// Reject paths that do not use this service's prefix (proxy domain or obs://).\n\t// parseObsFilePath falls back to returning the raw input for unknown prefixes,\n\t// so guard explicitly here to detect cross-backend sources.\n\tif !strings.HasPrefix(srcPath, s.getPrifix()) {\n\t\treturn \"\", fmt.Errorf(\"obs copy rejected source %q: %w\", srcPath, ErrCrossBackendCopy)\n\t}\n\tsrcKey, err := s.parseObsFilePath(srcPath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"obs copy rejected source %q: %w\", srcPath, ErrCrossBackendCopy)\n\t}\n\n\text := filepath.Ext(srcPath)\n\tvar destKey string\n\tif s.pathPrefix != \"\" {\n\t\tdestKey = fmt.Sprintf(\"%s/%d/%s/%s%s\", s.pathPrefix, tenantID, knowledgeID, uuid.New().String(), ext)\n\t} else {\n\t\tdestKey = fmt.Sprintf(\"%d/%s/%s%s\", tenantID, knowledgeID, uuid.New().String(), ext)\n\t}\n\n\t// CopySource is \"bucket/key\"; the '/' separators must NOT be percent-encoded\n\t// (url.PathEscape would turn them into %2F and break the bucket/key split).\n\t_, err = s.client.CopyObject(ctx, &s3.CopyObjectInput{\n\t\tBucket:     aws.String(s.bucketName),","sourceCodeStart":235,"sourceCodeEnd":271,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/application/service/file/obs.go#L235-L271","documentation":"CopyFile explicitly rejects source paths that do not start with this service's own prefix (proxy domain or obs:// scheme). Because parseObsFilePath silently returns unknown-format paths unchanged, this guard detects cross-backend copies (e.g. copying an OSS or local-disk path into OBS) and aborts with ErrCrossBackendCopy.","triggerScenarios":"Calling CopyFile with srcPath that does not begin with s.getPrifix() — e.g. a path produced by the OSS service, a local file path like /data/uploads/x.png, or a URL pointing at another bucket's proxy domain.","commonSituations":"Migrating between storage backends and passing old OSS paths to the OBS service; multi-tenant configs where different tenants use different storage services; copy-pasting a file URL from a different environment.","solutions":["Ensure srcPath was produced by this same OBS service (starts with proxyDomain prefix or obs:// prefix)","For genuine cross-backend copies, download via the source service and upload via SaveFile/SaveBytes instead of CopyFile","Check tenant configuration so each request uses the file service that owns the stored path","Validate the path prefix before calling and surface a clear 'unsupported source backend' message to the user"],"exampleFix":"// before\nnewPath, err := obsSvc.CopyFile(ctx, ossPath, tenantID, kid) // rejected\n// after\nif isOSSPath(ossPath) {\n    r, _ := ossSvc.GetFile(ctx, ossPath)\n    defer r.Close()\n    newPath, err = obsSvc.SaveBytes(ctx, tenantID, kid, readAll(r), ext)\n} else {\n    newPath, err = obsSvc.CopyFile(ctx, ossPath, tenantID, kid)\n}","handlingStrategy":"validation","validationCode":"func copySourceOK(srcPath, prefix string) bool {\n    return strings.HasPrefix(srcPath, prefix)\n}\n// call before CopyFile:\nif !copySourceOK(srcPath, obsPrefix) {\n    return ErrCrossBackendCopy\n}","typeGuard":"func isOBSBackendPath(p string) bool {\n    return strings.HasPrefix(p, \"obs://\") || strings.HasPrefix(p, proxyDomain)\n}","tryCatchPattern":"newPath, err := svc.CopyFile(ctx, src, tenantID, kid)\nif errors.Is(err, file.ErrCrossBackendCopy) {\n    http.Error(w, \"source file belongs to a different storage backend\", http.StatusBadRequest)\n    return\n}","preventionTips":["Keep a backend marker with each stored path so the right service is selected","Route all copy operations through a dispatcher that picks the owning file service","Never pass raw user URLs directly to CopyFile"],"tags":["obs","copy","cross-backend","validation"],"backgroundTag":"cross-backend-copy-rejected","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}