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

  1. 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).
  2. Free space on / ensure write permission for util.TempDir and the target notebook data dir.
  3. If a specific asset is problematic (name/encoding), rename it in the source vault to a portable name, re-analyse, then import.
  4. 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

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


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/067e7ed2f2c40c68. Report an issue: GitHub.