{"record":{"id":"067e7ed2f2c40c68","repo":"siyuan-note/siyuan","slug":"348","errorCode":"348","errorMessage":"copy attachment [%s]: %w","messagePattern":"copy attachment \\[(.+?)\\]: %w","errorType":"error_code","errorClass":"obsidianUserError","httpStatus":null,"severity":"error","filePath":"kernel/model/import_obsidian.go","lineNumber":1684,"sourceCode":"\t\tfailObsidianTask(taskID, \"staging\", err, nil)\n\t\tremoveObsidianTemp(taskID)\n\t\treturn\n\t}\n\tvar assetKeys []string\n\tfor key := range vault.ImportAssets {\n\t\tassetKeys = append(assetKeys, key)\n\t}\n\tsort.Strings(assetKeys)\n\tfor index, key := range assetKeys {\n\t\tif err := ctx.Err(); err != nil {\n\t\t\tremoveObsidianTemp(taskID)\n\t\t\treturn\n\t\t}\n\t\tasset := vault.ImportAssets[key]\n\t\tdestination := filepath.Join(assetsTemp, asset.FinalName)\n\t\tif err := copyStableObsidianFile(asset.Source, destination); err != nil {\n\t\t\tfailObsidianTask(taskID, \"staging\", newObsidianUserError(348, asset.Source.RelPath,\n\t\t\t\tfmt.Errorf(\"copy attachment [%s]: %w\", asset.Source.RelPath, err)), nil)\n\t\t\tremoveObsidianTemp(taskID)\n\t\t\treturn\n\t\t}\n\t\tresult.ImportedAttachmentCount++\n\t\tupdateObsidianTask(taskID, ObsidianTaskStateStaging, 65+(index+1)*15/maxInt(len(assetKeys), 1), \"Copying attachments\")\n\t}\n\tif err := writeObsidianSortFile(tempRoot, vault.Docs); err != nil {\n\t\tfailObsidianTask(taskID, \"staging\", err, nil)\n\t\tremoveObsidianTemp(taskID)\n\t\treturn\n\t}\n\tif err := ctx.Err(); err != nil {\n\t\tremoveObsidianTemp(taskID)\n\t\treturn\n\t}\n\n\tif !updateObsidianTask(taskID, ObsidianTaskStateCreating, 82, \"Creating notebook\") {\n\t\tremoveObsidianTemp(taskID)","sourceCodeStart":1666,"sourceCodeEnd":1702,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/import_obsidian.go#L1666-L1702","documentation":"During staging, copyStableObsidianFile(asset.Source, destination) fails for an attachment and is wrapped with code 348 ('Unable to import asset file [%s]'). copyStableObsidianFile validates source metadata before and after the copy (validateObsidianSourceMetadata) and uses filelock.Copy; failure can come from either the metadata drift guard, MkdirAll on the destination, or the underlying copy.","triggerScenarios":"Attachment deleted/changed between analysis and staging (metadata guard fires), destination disk full or read-only, permission denied reading the source asset, or a path-too-long/invalid-name error when creating the destination under the temp tree. Each referenced asset is copied in sorted order, so the first failure aborts.","commonSituations":"Editing the vault (adding/removing attachments) after analysis; destination volume out of space; source asset on a disconnected external drive; asset filename contains characters the OS rejects on the target FS; antivirus blocking the copy.","solutions":["Do not modify the vault between analysis and import; if assets changed, re-run StartObsidianVaultAnalysis to get a consistent snapshot (clears code 346/349 drift).","Free space on / ensure write permission for util.TempDir and the target notebook data dir.","If a specific asset is problematic (name/encoding), rename it in the source vault to a portable name, re-analyse, then import.","Check the kernel log for the underlying copy error (ENOSPC, EACCES, EROFS) and address that resource."],"exampleFix":"// before: asset edited/removed after analysis -> metadata guard fails\n//   copyStableObsidianFile -> validateObsidianSourceMetadata error\n\n// after: re-analyse to snapshot current state, then import immediately\nStartObsidianVaultAnalysis(vaultPath) // fresh task\n// (do not touch the vault) then StartObsidianVaultImport(taskID, notebook)","handlingStrategy":"validation","validationCode":"// Confirm assets are readable and the dest volume has free space.\nfunc assetsCopyable(vault, dest string) error {\n    free, _ := diskFreeBytes(dest)\n    need := uint64(0)\n    _ = filepath.WalkDir(vault, func(p string, d fs.DirEntry, err error) error {\n        if err == nil && !d.IsDir() { i, _ := d.Info(); need += i.Size() }\n        return nil\n    })\n    if need > free { return fmt.Errorf(\"need %d bytes, have %d free\", need, free) }\n    return nil\n}","typeGuard":"null","tryCatchPattern":"null","preventionTips":["Free space and ensure write permission on util.TempDir and the data dir.","Pause sync/antivirus that may rewrite or lock assets during staging.","Re-analyse if assets change so the copy plan matches reality."],"tags":["obsidian-import","staging","assets","copy","filesystem"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}