{"record":{"id":"762eb6682e40be26","repo":"siyuan-note/siyuan","slug":"invalid-export-path-762eb6","errorCode":null,"errorMessage":"invalid export path","messagePattern":"invalid export path","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/notebook_bundle.go","lineNumber":188,"sourceCode":"\t}\n\tclosed = true\n\n\tfinalPath := filepath.Join(util.TempDir, \"export\", baseFolderName+\".sy.zip\")\n\tif err = os.Remove(finalPath); nil != err && !os.IsNotExist(err) {\n\t\tlogging.LogErrorf(\"remove previous notebook bundle failed: %s\", err)\n\t\treturn \"\"\n\t}\n\tif err = os.Rename(partialPath, finalPath); nil != err {\n\t\tlogging.LogErrorf(\"publish notebook bundle failed: %s\", err)\n\t\treturn \"\"\n\t}\n\treturn \"/export/\" + url.PathEscape(filepath.Base(finalPath))\n}\n\nfunc exportedFilePath(exportPath string) (ret string, err error) {\n\tencoded, ok := strings.CutPrefix(exportPath, \"/export/\")\n\tif !ok || encoded == \"\" {\n\t\treturn \"\", errors.New(\"invalid export path\")\n\t}\n\tdecoded, err := url.PathUnescape(encoded)\n\tif nil != err {\n\t\treturn \"\", err\n\t}\n\tret = filepath.Join(util.TempDir, \"export\", filepath.FromSlash(decoded))\n\tif !gulu.File.IsSubPath(filepath.Join(util.TempDir, \"export\"), ret) {\n\t\treturn \"\", errors.New(\"export path is outside export directory\")\n\t}\n\treturn\n}\n\n// ImportSYNotebookBundle 导入批量笔记本包。普通 .sy.zip 返回 bundle=false，由原有导入流程继续处理。\nfunc ImportSYNotebookBundle(zipPath string) (boxIDs []string, bundle bool, err error) {\n\tarchive, openErr := zip.OpenReader(zipPath)\n\tif nil != openErr {\n\t\terr = openErr\n\t\treturn","sourceCodeStart":170,"sourceCodeEnd":206,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/notebook_bundle.go#L170-L206","documentation":"exportedFilePath reverses the '/export/' URL produced by exportedFileURL: it strips the prefix, URL-decodes the remainder, and joins it under the kernel temp export dir. If exportPath does not start with '/export/' or the encoded part is empty, it returns 'invalid export path' — the caller passed a path that never came from the bundle exporter.","triggerScenarios":"exportNotebooksSYBundle (or tests) calling exportedFilePath with a string lacking the '/export/' prefix, an empty suffix ('/export/'), or a hand-built relative path.","commonSituations":"Scripts storing/replaying export URLs after modifying them; passing a filesystem path instead of the '/export/...' URL; empty-name entries produced by upstream listing bugs.","solutions":["Pass the exact URL string returned by exportedFileURL, unmodified","Skip entries whose path does not start with '/export/'","Ensure the encoded segment is non-empty before calling"],"exampleFix":"// before\nexportedFilePath(entry.path) // entry.path = 'assets/foo.png'\n// after\nif (strings.HasPrefix(entry.path, \"/export/\")) {\n    exportedFilePath(entry.path)\n}","handlingStrategy":"validation","validationCode":"function isExportURL(p){ return typeof p === 'string' && p.startsWith('/export/') && p.length > '/export/'.length; }","typeGuard":"function asExportPath(v){ return typeof v === 'string' && v.startsWith('/export/') && v.length > 8 ? v : null; }","tryCatchPattern":"ret, err := exportedFilePath(p); if err != nil { if errors.Is(err, errInvalidExportPath) || err.Error() == \"invalid export path\" { skipEntry(p); return; } return err; }","preventionTips":["Reuse URLs exactly as produced by the exporter","Filter entries by the '/export/' prefix before processing","Don't concatenate your own export paths"],"tags":["export","path","validation"],"backgroundTag":"invalid-argument-format","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}