{"record":{"id":"b09c61366540af3f","repo":"AlistGo/alist","slug":"obj-already-exists","errorCode":null,"errorMessage":"obj already exists","messagePattern":"obj already exists","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/op/fs.go","lineNumber":613,"sourceCode":"\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t} else {\n\t\t\t\tkey := Key(storage, stdpath.Join(dstDirPath, file.GetName()))\n\t\t\t\tlinkCache.Del(key)\n\t\t\t}\n\t\t}\n\t}\n\treturn errors.WithStack(err)\n}\n\nfunc PutURL(ctx context.Context, storage driver.Driver, dstDirPath, dstName, url string, lazyCache ...bool) error {\n\tif storage.Config().CheckStatus && storage.GetStorage().Status != WORK {\n\t\treturn errors.Errorf(\"storage not init: %s\", storage.GetStorage().Status)\n\t}\n\tdstDirPath = utils.FixAndCleanPath(dstDirPath)\n\t_, err := GetUnwrap(ctx, storage, stdpath.Join(dstDirPath, dstName))\n\tif err == nil {\n\t\treturn errors.New(\"obj already exists\")\n\t}\n\terr = MakeDir(ctx, storage, dstDirPath)\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed to put url\")\n\t}\n\tdstDir, err := GetUnwrap(ctx, storage, dstDirPath)\n\tif err != nil {\n\t\treturn errors.WithMessagef(err, \"failed to put url\")\n\t}\n\tswitch s := storage.(type) {\n\tcase driver.PutURLResult:\n\t\tvar newObj model.Obj\n\t\tnewObj, err = s.PutURL(ctx, dstDir, dstName, url)\n\t\tif err == nil {\n\t\t\tif newObj != nil {\n\t\t\t\taddCacheObj(storage, dstDirPath, model.WrapObjName(newObj))\n\t\t\t} else if !utils.IsBool(lazyCache...) {\n\t\t\t\tClearCache(storage, dstDirPath)","sourceCodeStart":595,"sourceCodeEnd":631,"githubUrl":"https://github.com/AlistGo/alist/blob/843d9dc8149126976b2625911e45a4d3ffd6f2f5/internal/op/fs.go#L595-L631","documentation":"PutURL checks that the final object (dstDirPath/dstName) does not already exist before fetching by URL; if a GetUnwrap on that path succeeds, it returns 'obj already exists' rather than overwriting. This is an overwrite-protection guard, mirroring Put semantics for URL-based transfers.","triggerScenarios":"Calling op.PutURL with a dstName that already exists in dstDirPath — e.g. re-running an offline transfer or URL upload to the same target without deleting the previous output.","commonSituations":"Retried offline-download/transfer jobs that do not generate unique names; duplicate submissions of the same link; idempotent-retry logic that expects overwrite but gets refusal.","solutions":["Delete or rename the existing object, then retry the PutURL","Generate a unique destination name (suffix with timestamp or task id) for retries","Pre-check existence in your job logic and skip if the object is already the desired result"],"exampleFix":"// before\nerr := op.PutURL(ctx, storage, dir, \"movie.mp4\", url)\n// after: skip when the target already exists\nif _, err := op.GetUnwrap(ctx, storage, stdpath.Join(dir, \"movie.mp4\")); err == nil {\n    return nil // already transferred\n}\nerr := op.PutURL(ctx, storage, dir, \"movie.mp4\", url)","handlingStrategy":"validation","validationCode":"// Before PutURL\nif _, err := op.GetUnwrap(ctx, storage, stdpath.Join(dstDirPath, dstName)); err == nil {\n    return nil // already exists — skip or rename\n}","typeGuard":null,"tryCatchPattern":"if err := op.PutURL(ctx, storage, dir, name, u); err != nil {\n    if strings.Contains(err.Error(), \"obj already exists\") {\n        return nil // treat retry as success if idempotency is desired\n    }\n    return err\n}","preventionTips":["Make transfer jobs idempotent: pre-check target existence","Use unique names (timestamp/task-id suffix) for retried submissions","Decide overwrite policy up front and encode it in job configuration"],"tags":["upload","puturl","overwrite-protection"],"backgroundTag":null,"analyzedSha":"843d9dc8149126976b2625911e45a4d3ffd6f2f5","analyzedAt":"2026-08-15T12:14:11.722Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}