siyuan-note/siyuan · error · obsidianUserError
348
348
Error message
copy attachment [%s]: %w
What it means
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.
Source
Thrown at kernel/model/import_obsidian.go:1684
failObsidianTask(taskID, "staging", err, nil)
removeObsidianTemp(taskID)
return
}
var assetKeys []string
for key := range vault.ImportAssets {
assetKeys = append(assetKeys, key)
}
sort.Strings(assetKeys)
for index, key := range assetKeys {
if err := ctx.Err(); err != nil {
removeObsidianTemp(taskID)
return
}
asset := vault.ImportAssets[key]
destination := filepath.Join(assetsTemp, asset.FinalName)
if err := copyStableObsidianFile(asset.Source, destination); err != nil {
failObsidianTask(taskID, "staging", newObsidianUserError(348, asset.Source.RelPath,
fmt.Errorf("copy attachment [%s]: %w", asset.Source.RelPath, err)), nil)
removeObsidianTemp(taskID)
return
}
result.ImportedAttachmentCount++
updateObsidianTask(taskID, ObsidianTaskStateStaging, 65+(index+1)*15/maxInt(len(assetKeys), 1), "Copying attachments")
}
if err := writeObsidianSortFile(tempRoot, vault.Docs); err != nil {
failObsidianTask(taskID, "staging", err, nil)
removeObsidianTemp(taskID)
return
}
if err := ctx.Err(); err != nil {
removeObsidianTemp(taskID)
return
}
if !updateObsidianTask(taskID, ObsidianTaskStateCreating, 82, "Creating notebook") {
removeObsidianTemp(taskID)View on GitHub (pinned to 251596fc0d)
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.
Example fix
// before: asset edited/removed after analysis -> metadata guard fails // copyStableObsidianFile -> validateObsidianSourceMetadata error // after: re-analyse to snapshot current state, then import immediately StartObsidianVaultAnalysis(vaultPath) // fresh task // (do not touch the vault) then StartObsidianVaultImport(taskID, notebook)
Defensive patterns
Strategy: validation
Validate before calling
// Confirm assets are readable and the dest volume has free space.
func assetsCopyable(vault, dest string) error {
free, _ := diskFreeBytes(dest)
need := uint64(0)
_ = filepath.WalkDir(vault, func(p string, d fs.DirEntry, err error) error {
if err == nil && !d.IsDir() { i, _ := d.Info(); need += i.Size() }
return nil
})
if need > free { return fmt.Errorf("need %d bytes, have %d free", need, free) }
return nil
} Type guard
null
Try / catch
null
Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- read image failed: %w
- path is not a child of assets directory: %s
- resolve assets directory [%s] failed: %w
- resolve asset [%s] failed: %w
- asset path resolves outside assets directory: %s
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/067e7ed2f2c40c68.
Report an issue: GitHub.