diff options
Diffstat (limited to 'packages/shared/import-export/importer.ts')
| -rw-r--r-- | packages/shared/import-export/importer.ts | 158 |
1 files changed, 158 insertions, 0 deletions
diff --git a/packages/shared/import-export/importer.ts b/packages/shared/import-export/importer.ts new file mode 100644 index 00000000..88c0c3bc --- /dev/null +++ b/packages/shared/import-export/importer.ts @@ -0,0 +1,158 @@ +import { limitConcurrency } from "../concurrency"; +import { MAX_LIST_NAME_LENGTH } from "../types/lists"; +import { ImportSource, ParsedBookmark, parseImportFile } from "./parsers"; + +export interface ImportCounts { + successes: number; + failures: number; + alreadyExisted: number; + total: number; +} + +export interface ImportDeps { + createList: (input: { + name: string; + icon: string; + parentId?: string; + }) => Promise<{ id: string }>; + createBookmark: ( + bookmark: ParsedBookmark, + ) => Promise<{ id: string; alreadyExists?: boolean }>; + addBookmarkToLists: (input: { + bookmarkId: string; + listIds: string[]; + }) => Promise<void>; + updateBookmarkTags: (input: { + bookmarkId: string; + tags: string[]; + }) => Promise<void>; +} + +export interface ImportOptions { + concurrencyLimit?: number; + parsers?: Partial< + Record<ImportSource, (textContent: string) => ParsedBookmark[]> + >; +} + +export interface ImportResult { + counts: ImportCounts; + rootListId: string | null; +} + +export async function importBookmarksFromFile( + { + file, + source, + rootListName, + deps, + onProgress, + }: { + file: { text: () => Promise<string> }; + source: ImportSource; + rootListName: string; + deps: ImportDeps; + onProgress?: (done: number, total: number) => void; + }, + options: ImportOptions = {}, +): Promise<ImportResult> { + const { concurrencyLimit = 20, parsers } = options; + + const textContent = await file.text(); + const parsedBookmarks = parsers?.[source] + ? parsers[source]!(textContent) + : parseImportFile(source, textContent); + if (parsedBookmarks.length === 0) { + return { + counts: { successes: 0, failures: 0, alreadyExisted: 0, total: 0 }, + rootListId: null, + }; + } + + const rootList = await deps.createList({ name: rootListName, icon: "⬆️" }); + + onProgress?.(0, parsedBookmarks.length); + + const PATH_DELIMITER = "$$__$$"; + + // Build required paths + const allRequiredPaths = new Set<string>(); + for (const bookmark of parsedBookmarks) { + for (const path of bookmark.paths) { + if (path && path.length > 0) { + for (let i = 1; i <= path.length; i++) { + const subPath = path.slice(0, i); + const pathKey = subPath.join(PATH_DELIMITER); + allRequiredPaths.add(pathKey); + } + } + } + } + + const allRequiredPathsArray = Array.from(allRequiredPaths).sort( + (a, b) => a.split(PATH_DELIMITER).length - b.split(PATH_DELIMITER).length, + ); + + const pathMap: Record<string, string> = { "": rootList.id }; + + for (const pathKey of allRequiredPathsArray) { + const parts = pathKey.split(PATH_DELIMITER); + const parentKey = parts.slice(0, -1).join(PATH_DELIMITER); + const parentId = pathMap[parentKey] || rootList.id; + + const folderName = parts[parts.length - 1]; + const folderList = await deps.createList({ + name: folderName.substring(0, MAX_LIST_NAME_LENGTH), + parentId, + icon: "📁", + }); + pathMap[pathKey] = folderList.id; + } + + const importPromises = parsedBookmarks.map((bookmark) => async () => { + const listIds = bookmark.paths.map( + (path) => pathMap[path.join(PATH_DELIMITER)] || rootList.id, + ); + if (listIds.length === 0) listIds.push(rootList.id); + + const created = await deps.createBookmark(bookmark); + await deps.addBookmarkToLists({ bookmarkId: created.id, listIds }); + if (bookmark.tags && bookmark.tags.length > 0) { + await deps.updateBookmarkTags({ + bookmarkId: created.id, + tags: bookmark.tags, + }); + } + + return created; + }); + + const resultsPromises = limitConcurrency(importPromises, concurrencyLimit); + const results = await Promise.allSettled(resultsPromises); + + let successes = 0; + let failures = 0; + let alreadyExisted = 0; + + let done = 0; + for (const r of results) { + if (r.status === "fulfilled") { + if (r.value.alreadyExists) alreadyExisted++; + else successes++; + } else { + failures++; + } + done += 1; + onProgress?.(done, parsedBookmarks.length); + } + + return { + counts: { + successes, + failures, + alreadyExisted, + total: parsedBookmarks.length, + }, + rootListId: rootList.id, + }; +} |
