import events from "events" ; import fs from "fs" ; import os from "os" ; import path from "path" ; import sqlite from "sqlite" ; import unzip , { Entry } from "unzipper" ; import { Card , Collection , Log , Note } from "./ankiDBTypes" ; async function readRows < R > ( handle , tableName , processRow ) { const promises = [ ] ; await ( handle as sqlite . Database ) . each ( `SELECT * FROM ${ tableName }` , ( error , row ) => { if ( error ) { throw error ; } else { promises . push ( processRow ( row ) ) ; } } ) ; return Promise . all ( promises ) ; } export function readNotes ( handle , visitor ) { return readRows ( handle , "notes" , visitor ) ; } export function readCards ( handle , visitor ) { return readRows ( handle , "cards" , visitor ) ; } export function readLogs ( handle , visitor ) { return readRows ( handle , "revlog" , visitor ) ; } export type AnkiCollectionDBHandle = unknown ; export async function readCollection ( handle ) { const database = handle as sqlite . Database ; const rawCollection = await database . get ( "SELECT * FROM col" ) ; for ( const key of [ "conf" , "models" , "decks" , "dconf" ] ) { try { rawCollection [ key ] = JSON . parse ( rawCollection [ key ] ) ; } catch ( error ) { throw new Error ( `Error parsing ${ key }: ${ error }` ) ; } } return rawCollection ; } async function withCollectionDatabase < R = undefined > ( ankiCollectionPath , continuation ) { const database = await sqlite . open ( ankiCollectionPath , { mode : 1 } ) ; return continuation ( database ) ; } export interface MediaManifest { [ key ] : string ; } export async function readAnkiCollectionPackage < R > ( collectionPath , continuation ) { const workingDir = path . resolve ( os . tmpdir ( ) , `anki-import-${ Date . now ( ) }` ) ; await fs . promises . mkdir ( workingDir , { recursive : true } ) ; let ankiCollectionWritePath = null ; let mediaManifest = null ; const attachmentIDsToExtractedPaths = { } ; const promises = [ ] ; function queueExtractEntry ( entry ) { const targetPath = path . resolve ( workingDir , entry . path ) ; const writeStream = fs . createWriteStream ( targetPath ) ; entry . pipe ( writeStream ) ; promises . push ( events . once ( writeStream , "close" ) ) ; return targetPath ; } const collectionReadStream = fs . createReadStream ( collectionPath ) . pipe ( unzip . Parse ( ) ) . on ( "entry" , ( entry ) => { if ( entry . path === "collection.anki2" ) { ankiCollectionWritePath = queueExtractEntry ( entry ) ; } else if ( entry . path === "media" ) { entry . setEncoding ( "utf-8" ) ; let mediaManifestContents = "" ; entry . on ( "data" , ( chunk ) => { mediaManifestContents += chunk ; } ) ; promises . push ( events . once ( entry , "end" ) . then ( ( ) => { mediaManifest = JSON . parse ( mediaManifestContents ) ; } ) ) ; } else { if ( isNaN ( Number . parseInt ( entry . path ) ) ) { entry . autodrain ( ) ; } else { attachmentIDsToExtractedPaths [ entry . path ] = queueExtractEntry ( entry ) ; } } } ) ; promises . push ( events . once ( collectionReadStream , "close" ) ) ; await Promise . all ( promises ) ; if ( ankiCollectionWritePath ) { return await withCollectionDatabase ( ankiCollectionWritePath , ( handle ) => continuation ( handle , mediaManifest , attachmentIDsToExtractedPaths ) ) ; } else { throw new Error ( `${ collectionPath } did not contain a collection database (probable version mismatch)` ) ; } }