siyuan-note/siyuan · error

asset path must be under assets

Error message

asset path must be under assets

What it means

Thrown by CreateAssetHistory when the cleaned assetPath does not start with the prefix "assets/". The function first cleans the path (filepath.Clean, ToSlash, trim leading "/"), then checks the prefix. This is the first line of defense ensuring only paths under the assets/ directory are accepted for history snapshotting. The check rejects paths like "data/file.txt", "temp/x", or any path not rooted under assets/.

Source

Thrown at kernel/model/history.go:869

	})
	return
}

func generateAssetsHistory() {
	assets := recentModifiedAssets()
	if 1 > len(assets) {
		return
	}
	if err := createAssetsHistory(assets); err != nil {
		logging.LogErrorf("generate assets history failed: %s", err)
	}
}

// CreateAssetHistory 为指定资源文件创建历史快照。
func CreateAssetHistory(assetPath string) (err error) {
	assetPath = strings.TrimPrefix(filepath.ToSlash(filepath.Clean(filepath.FromSlash(assetPath))), "/")
	if !strings.HasPrefix(assetPath, "assets/") {
		return errors.New("asset path must be under assets")
	}

	assetAbsPath := filepath.Join(util.DataDir, filepath.FromSlash(assetPath))
	assetsDir := filepath.Join(util.DataDir, "assets")
	if !gulu.File.IsSubPath(assetsDir, assetAbsPath) {
		return errors.New("asset path must be under assets")
	}
	info, statErr := os.Stat(assetAbsPath)
	if statErr != nil {
		return statErr
	}
	if info.IsDir() {
		return errors.New("asset path must be a file")
	}
	return createAssetsHistory([]string{assetAbsPath})
}

func createAssetsHistory(assets []string) (err error) {

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Ensure the assetPath argument is a relative path starting with "assets/" (e.g. "assets/image-202401010000.png").
  2. If the caller has only a filename, prefix it with "assets/" before calling the API.
  3. As an API client, validate the path starts with "assets/" before submitting.

Example fix

// before
await post('/api/history/createAssetHistory', { assetPath: 'image-202401010000.png' })

// after
await post('/api/history/createAssetHistory', { assetPath: 'assets/image-202401010000.png' })
Defensive patterns

Strategy: validation

Validate before calling

// Validate the asset path starts with 'assets/' before calling the API
function isValidAssetPath(p) {
  const cleaned = path.posix.normalize(p).replace(/^\//, '')
  return cleaned.startsWith('assets/')
}
if (isValidAssetPath(assetPath)) {
  await post('/api/history/createAssetHistory', { assetPath })
}

Type guard

// Type guard: check if a string is a valid asset path
function isAssetPath(p: string): boolean {
  const cleaned = p.replace(/^\//, '')
  return cleaned.startsWith('assets/')
}

Prevention

When it happens

Trigger: Calling POST /api/history/createAssetHistory (or equivalent) with an assetPath that does not begin with "assets/" after cleaning. For example: "file.txt", "/etc/passwd", "data/20240101/file.sy", or any non-asset path. The prefix check is literal string comparison after path normalization.

Common situations: An API client passes a full filesystem path or a SiYuan-internal path instead of the relative assets/ path; a plugin constructs the path incorrectly by omitting the "assets/" prefix; the user or tool references a document path (.sy) instead of an asset path.

Related errors


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