Skip to content

Commit

Permalink
Add initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
Avol-V committed Jan 15, 2023
1 parent ef48023 commit 30e9927
Show file tree
Hide file tree
Showing 21 changed files with 875 additions and 1 deletion.
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,34 @@
# linkace-import-firefox
Import Firefox JSON bookmarks to https://www.linkace.org/

Import Firefox JSON bookmarks to [LinkAce](https://www.linkace.org/) self-hosted bookmark archive.

## Installation

Clone and run `npm i`.

## Usage

With LinkAce .env in current working directory:
```
node index.js bookmarks.json
```
Or:

```
node index.js --bookmarks=bookmarks.json
```

With path to LinkAce .env file:
```
node index.js --env=/path/to/.env bookmarks.json
```
Or:
```
node index.js --env=/path/to/.env --bookmarks=bookmarks.json
```

To start from specific URL:

```
node index.js bookmarks.json --from=https://example.com
```
72 changes: 72 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { readFile } from 'node:fs/promises';
import dotenv from 'dotenv';
import { parseArgs } from './lib/parse-args.js';
import { getDbClient } from './lib/get-db-client.js';
import { importBookmarks } from './lib/import-bookmarks.js';

/**
* @typedef { import( './types/firefox-bookmarks.js' ).Bookmarks } Bookmarks
*/

async function main()
{
const args = parseArgs( process.argv.slice( 2 ) );

const bookmarksPath = args.bookmarks || args._[0];

if ( !bookmarksPath )
{
console.log(
`Usage:
With LinkAce .env in current working directory:
${process.argv[0]} ${process.argv[1]} bookmarks.json
Or:
${process.argv[0]} ${process.argv[1]} --bookmarks=bookmarks.json
With path to LinkAce .env file:
${process.argv[0]} ${process.argv[1]} --env=/path/to/.env bookmarks.json
Or:
${process.argv[0]} ${process.argv[1]} --env=/path/to/.env --bookmarks=bookmarks.json
To start from specific URL:
${process.argv[0]} ${process.argv[1]} bookmarks.json --from=https://example.com
`,
);

process.exit( 1 );
}

const jsonData = await readFile( bookmarksPath, 'utf8' );
/** @type { Bookmarks } */
const bookmarks = JSON.parse( jsonData );

bookmarks.title = 'Firefox';

dotenv.config({
path: args.env,
});

const client = getDbClient();

await client.connect();

try
{
const importedCount = await importBookmarks( client, bookmarks, args.from );

console.log( `Imported ${importedCount} bookmarks` );
}
finally
{
await client.end();
}
}

main()
.catch(
( error ) =>
{
console.error( error );
process.exit( 1 );
},
);
27 changes: 27 additions & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "esnext",
"module": "NodeNext",
"lib": ["esnext"],

"checkJs": true,
"noEmit": true,

"strict": true,

"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"forceConsistentCasingInFileNames": true,

"types": [
"node"
]
},
"exclude": [
"node_modules",
"**/node_modules/*"
]
}
16 changes: 16 additions & 0 deletions lib/db/insert-link-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* @typedef { import( 'pg' ).Client } Client
*/

/**
* @param { Client } client
* @param { number } linkId
* @param { number } listId
*/
export async function insertLinkList( client, linkId, listId )
{
await client.query(
'INSERT INTO link_lists(link_id, list_id) VALUES ($1, $2)',
[linkId, listId],
);
}
16 changes: 16 additions & 0 deletions lib/db/insert-link-tag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* @typedef { import( 'pg' ).Client } Client
*/

/**
* @param { Client } client
* @param { number } linkId
* @param { number } tagId
*/
export async function insertLinkTag( client, linkId, tagId )
{
await client.query(
'INSERT INTO link_tags(link_id, tag_id) VALUES ($1, $2)',
[linkId, tagId],
);
}
33 changes: 33 additions & 0 deletions lib/db/insert-link.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { getUserId } from '../get-user-id.js';

/**
* @typedef { import( 'pg' ).Client } Client
* @typedef { import( './insert-link.types.js' ).LinkRecord } LinkRecord
*/

/**
* @param { Client } client
* @param { LinkRecord } link
*/
export async function insertLink( client, link )
{
const userId = getUserId();
const icon = (
(
link.icon
&& ( link.icon.length < 191 )
)
? link.icon
: null
);
const result = await client.query(
`INSERT INTO
links(user_id, url, title, created_at, updated_at, icon)
VALUES
($1, $2, $3, $4, $5, $6)
RETURNING id`,
[userId, link.url, link.title, link.createdAt, link.updatedAt, icon],
);

return Number( result.rows[0].id );
}
7 changes: 7 additions & 0 deletions lib/db/insert-link.types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type LinkRecord = {
url: string,
title: string,
createdAt: Date,
updatedAt: Date,
icon?: string,
};
20 changes: 20 additions & 0 deletions lib/db/insert-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getUserId } from '../get-user-id.js';

/**
* @typedef { import( 'pg' ).Client } Client
*/

/**
* @param { Client } client
* @param { string } name
*/
export async function insertList( client, name )
{
const userId = getUserId();
const result = await client.query(
'INSERT INTO lists(user_id, name) VALUES ($1, $2) RETURNING id',
[userId, name],
);

return Number( result.rows[0].id );
}
20 changes: 20 additions & 0 deletions lib/db/insert-tag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getUserId } from '../get-user-id.js';

/**
* @typedef { import( 'pg' ).Client } Client
*/

/**
* @param { Client } client
* @param { string } tag
*/
export async function insertTag( client, tag )
{
const userId = getUserId();
const result = await client.query(
'INSERT INTO tags(user_id, name) VALUES ($1, $2) RETURNING id',
[userId, tag],
);

return Number( result.rows[0].id );
}
27 changes: 27 additions & 0 deletions lib/db/select-lists.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { getUserId } from '../get-user-id.js';

/**
* @typedef { import( 'pg' ).Client } Client
*/

/**
* @param { Client } client
*/
export async function selectLists( client )
{
const userId = getUserId();
const result = await client.query(
'SELECT id, name FROM lists WHERE user_id = $1',
[userId],
);

/** @type { Map<string, number> } */
const lists = new Map();

for ( const row of result.rows )
{
lists.set( row.name, row.id );
}

return lists;
}
27 changes: 27 additions & 0 deletions lib/db/select-tags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { getUserId } from '../get-user-id.js';

/**
* @typedef { import( 'pg' ).Client } Client
*/

/**
* @param { Client } client
*/
export async function selectTags( client )
{
const userId = getUserId();
const result = await client.query(
'SELECT id, name FROM tags WHERE user_id = $1',
[userId],
);

/** @type { Map<string, number> } */
const tags = new Map();

for ( const row of result.rows )
{
tags.set( row.name, row.id );
}

return tags;
}
51 changes: 51 additions & 0 deletions lib/get-bookmarks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @typedef { import( '../types/firefox-bookmarks.js' ).Bookmarks } Bookmarks
* @typedef { import( './get-bookmarks.types.js' ).ContainerWithTitles } ContainerWithTitles
* @typedef { import( './get-bookmarks.types.js' ).BookmarkWithTitles } BookmarkWithTitles
*/

/**
* @param { Bookmarks } bookmarks
* @returns { Generator<BookmarkWithTitles, void, void> }
*/
export function* getBookmarks( bookmarks )
{
/** @type { Array<ContainerWithTitles> } */
const items = [bookmarks];
/** @type { ContainerWithTitles | undefined } */
let current;

while ( (current = items.pop()) )
{
if ( current.children === undefined )
{
continue;
}

const currentTitles = [
...(current.titles ?? []),
current.title,
];

for ( const child of current.children )
{
if ( child.type === 'text/x-moz-place' )
{
if ( child.uri.startsWith( 'place:' ) )
{
continue;
}

/** @type { BookmarkWithTitles } */(child).titles = currentTitles;

yield child;
}
else
{
/** @type { ContainerWithTitles } */(child).titles = currentTitles;

items.push( child );
}
}
}
}
12 changes: 12 additions & 0 deletions lib/get-bookmarks.types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type {
ItemContainer,
ItemPlace,
} from '../types/firefox-bookmarks.js';

export type ContainerWithTitles = ItemContainer & {
titles?: Array<string>,
};

export type BookmarkWithTitles = ItemPlace & {
titles?: Array<string>,
};
17 changes: 17 additions & 0 deletions lib/get-db-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import pg from 'pg';

export function getDbClient()
{
if ( process.env.DB_CONNECTION !== 'pgsql' )
{
throw new Error( 'Only PostgreSQL database supported' );
}

return new pg.Client({
host: process.env.DB_HOST,
port: process.env.DB_PORT ? Number( process.env.DB_PORT ) : undefined,
database: process.env.DB_DATABASE,
user: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
});
}
4 changes: 4 additions & 0 deletions lib/get-user-id.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export function getUserId()
{
return process.env.LINKACE_USER_ID ? Number( process.env.LINKACE_USER_ID ) : 1;
}
Loading

0 comments on commit 30e9927

Please sign in to comment.