siyuan-note/siyuan · error

export artifact [%s] is a directory

Error message

export artifact [%s] is a directory

What it means

registerMobileExportLeaseWithID stats the artifact path before registering a lease; if the path exists but is a directory, the lease cannot be handed out (a lease is for a single exportable file) and the function returns this error wrapping the artifact path. It guards the invariant that leased export artifacts are regular files.

Source

Thrown at kernel/model/encrypted_export.go:368

	}
	return ""
}

func registerMobileExportLease(boxID, artifact, name, cleanupDir string) (*MobileExportLease, error) {
	leaseID, err := newManagedEncryptedExportID()
	if err != nil {
		return nil, err
	}
	return registerMobileExportLeaseWithID(leaseID, boxID, artifact, name, cleanupDir)
}

func registerMobileExportLeaseWithID(leaseID, boxID, artifact, name, cleanupDir string) (*MobileExportLease, error) {
	info, err := os.Stat(artifact)
	if err != nil {
		return nil, err
	}
	if info.IsDir() {
		return nil, fmt.Errorf("export artifact [%s] is a directory", artifact)
	}
	state := &mobileExportLeaseState{
		boxID:      boxID,
		cleanupDir: cleanupDir,
		expiresAt:  time.Now().Add(mobileExportLeaseTTL),
	}
	mobileExportLeases.Lock()
	if _, exists := mobileExportLeases.leases[leaseID]; exists {
		mobileExportLeases.Unlock()
		return nil, fmt.Errorf("mobile export lease [%s] already exists", leaseID)
	}
	mobileExportLeases.leases[leaseID] = state
	state.timer = time.AfterFunc(mobileExportLeaseTTL, func() {
		releaseMobileExportLease(leaseID, true)
	})
	mobileExportLeases.Unlock()
	return &MobileExportLease{ID: leaseID, Path: artifact, Name: name, Size: info.Size()}, nil
}

View on GitHub (pinned to 8641553a1f)

Solutions

  1. os.Stat the artifact yourself before requesting the lease and reject directories with a clearer message.
  2. Point the lease at the concrete file inside the directory instead of the directory itself.
  3. Clean stale entries under temp/export (or the managed export output dir) that are directories where a file was expected.

Example fix

// before
lease, err := model.AcquireMobileExportLease("export/code")
// after
lease, err := model.AcquireMobileExportLease("export/code/main.txt")
Defensive patterns

Strategy: validation

Validate before calling

if info, err := os.Stat(artifactPath); err == nil && info.IsDir() {
    return fmt.Errorf("refusing to lease directory %q", artifactPath)
}

Prevention

When it happens

Trigger: registerMobileExportLease / AcquireExportArtifactLease is invoked with an artifact path that resolves to a directory — e.g. temp/export/<dir>, a resolved "managed export" path pointing at a folder, or an asset path that os.Stat sees as a directory.

Common situations: A stale temp/export entry where a previous export created a folder; misconfigured managed-export resolution returning the export output directory instead of a file; passing a folder-like asset path or a path with a trailing separator.

Related errors


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/89191e797e6c57b7. Report an issue: GitHub.