UNPKG

bookmate

Version:

WIP - Complete API for Chrome bookmatearks: create, read, update, delete and change notifications.

2 lines (1 loc) 12.6 kB
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});const e=require("crypto"),o=require("os"),t=require("path"),r=require("fs"),n=require("util"),i=e=>e&&"object"==typeof e&&"default"in e?e:{default:e},a=i(e),s=i(o),d=i(t),l=i(r),u=i(n).default.getSystemErrorMap(),c=[...u.entries()].reduce(((e,[o,[t,r]])=>(e.set(t,{errno:o,message:r}),e)),new Map);c.default=c.has("EINVAL")?"EINVAL":[...c.keys()].sort()[0],u.default=c.get(c.default).errno;class f extends Error{constructor(e=c.default,o){if(!c.has(e))throw new TypeError(`class SystemError can only be instantiated with one of the following error codes: ${[...c.keys()].join(", ")}`);super();const{errno:t,message:r}=c.get(e);this.errno=t,this.message=`${e}: ${r}`,o&&(this.message+=`\nAdditional information: ${o}`)}}const h=process.env.DEBUG_BOOKMATE||0,p=new Date(1601,0,1).getTime(),m=["checksum","roots","sync_metadata","version"],y={win:"%LOCALAPPDATA%\\Google\\Chrome\\User Data",winxp:"%USERPROFILE%\\Local Settings\\Application Data\\Google\\Chrome\\User Data",macos:d.default.resolve(s.default.homedir(),"Library/Application Support/Google/Chrome"),nix:d.default.resolve(s.default.homedir(),".config/google-chrome"),chromeos:"/home/chronos",ios:"Library/Application Support/Google/Chrome"},k={darwin:"macos",linux:"nix"},w={persistent:!1,unref:!0},g={active:new Set,books:{},maxId:{},mostRecentMountPoint:null},b=/^(Default|Profile \d+)$/i,S=e=>b.test(e),v=/^Bookmarks$/i,x=e=>v.test(e),E={bookmarkChanges:N,getProfileRootDir:K,tryToFindBookmarksLocation:H,mount:A,unmount:$,mkdirSync:P,readFileSync:T,readdirSync:R,writeFileSync:M,saveWithChecksum:C,promisesWatch:O};async function*N(e={}){const o=K();if(!l.default.existsSync(o))throw new TypeError(`Sorry! The directory where we thought the Chrome profile directories may be found (${o}), does not exist. We can't monitor changes to your bookmarks, so Bookmark Select Mode is not supported.`);const t=[],r=[];let n=!1,i=!1,a=!1,s=!1;const u=l.default.readdirSync(o,{withFileTypes:!0}).reduce(((e,t)=>{if(t.isDirectory()&&S(t.name)){const r=d.default.resolve(o,t.name);l.default.existsSync(r)&&e.push(r)}return e}),[]);for(const o of u){const s=d.default.resolve(o,"Bookmarks");l.default.existsSync(s)&&I(s);const u=Object.assign({},w,e),c=l.default.watch(o,u);h&&console.info(u),console.log(`Observing ${o}`),u.unref&&c.unref(),c.on("change",((e,t)=>{t=t||"";const a=d.default.resolve(o,t);h&&console.log(e,a),x(t)&&(g.active.has(a)||g.active.add(a),g.books[s]||I(s),g.mostRecentMountPoint=a,h&&console.log(e,a,i),n={event:e,path:a},r.pop(),i&&i())})),c.on("error",(e=>{console.warn(`Bookmark file observer for ${o} error`,e),t.length?i&&i():a&&a()})),c.on("close",(()=>{console.info(`Observer for ${o} closed`),t.length?i&&i():a&&a()})),t.push(c)}process.on("SIGTERM",c),process.on("SIGHUP",c),process.on("SIGINT",c),process.on("SIGBRK",c);e:for(;;){if(n){const{path:e}=n;n=!1;try{const o=Q(JSON.parse(l.default.readFileSync(e)),{toMap:!0,map:g.books[e]});for(const e of o)yield e}catch(e){console.warn("Error publishing Bookmarks changes",e)}}try{await new Promise(((e,o)=>{i=e,a=o,r.push({res:e,rej:o})}))}catch{r.pop();break e}}return c(),!0;async function c(){if(!s){for(s=!0,console.log("Bookmark observer shutting down...");r.length;)try{r.pop().rej()}finally{}for(a&&setTimeout((()=>a("bookmark watching stopped")),0);t.length;)try{t.pop().close()}finally{}console.log("Bookmark observer shut down cleanly.")}}}function A(e){if(!l.default.existsSync(e)||!x(d.default.basename(e)))throw new TypeError(`Could not remount onto ${e} because there was no Bookmark file there.`);I(e),g.fixedMountPoint=e}function $(){g.fixedMountPoint&&(g.books[g.fixedMountPoint]=null,g.fixedMountPoint=!1)}function B(){return g.fixedMountPoint||g.mostRecentMountPoint}function I(e){const o=l.default.readFileSync(e),t=JSON.parse(o),r={};return g.books[e]=Q(t,{toMap:!0,saved:r}),g.maxId[e]=r.maxId+1,g.mostRecentMountPoint=e,g.books[e]}async function*O(e){yield*N(e)}function P(e){if(L(e))return;const o=(e=J(e)).pop(),t=_({name:o},{strict:"folder"});if(!e.length)throw new f("EPERM",`You cannot add a Bookmark or a folder to the root.Adding a Bookmark like ${o} requires you specify a path of folders, not just a URL.`);{const o={},r=U(e,o);if(!r)throw new f("ENOENT",`The path ${e} did not exist.`);t.id=""+g.maxId[o.mountPoint]++,r.children.push(t),V(o.mountPoint,o.bookmarkObj)}}function L(e){return h&&console.log("existsSync"),void 0!==U(e)}function T(e,{encoding:o}={}){h&&console.log("readFileSync");let t=function(e){return U(e=W(e))}(e);if(t)return"json"!==o&&(t=Buffer.from(JSON.stringify(t)),o&&"buffer"!==o&&(t=t.toString(o))),t;throw new f("ENOENT",`Bookmark ${e} does not exist`)}function R(e,{withFileTypes:o,encoding:t}={}){h&&console.log("readdirSync");let r=function(e){return U(e=J(e))}(e);if(r){const e=e=>"buffer"===t?Buffer.from(e):e;return o?r.children.map((e=>("folder"===e.type&&delete e.children,e))):r.children.map((o=>Object.prototype.hasOwnProperty.call(o,"url")?e(o.url):e(o.name)))}throw new f("ENOENT",`Folder ${e} does not exist`)}function M(e,o){h&&console.log("writeFileSync"),e=W(e),o=_(o,{strict:"url"});const t=e.pop();if(!e.length)throw new f("EPERM",`You cannot add a Bookmark to the root.Adding a Bookmark like ${t} requires you specify a path of folders, not just a URL.`);{const r={},n=U(e,r);if(!n)throw new f("ENOENT",`The path ${e} did not exist.`);o.url=t,o.id=o.id||""+g.maxId[r.mountPoint]++;const i=n.children.findIndex((e=>e.id===o.id));i>-1?n.children.splice(i,1,o):n.children.push(o),V(r.mountPoint,r.bookmarkObj)}}function _(e,{strict:o}={}){let t;if(Buffer.isBuffer(e)&&(e=e.toString()),"string"==typeof e&&(e=JSON.parse(e)),!function(e){try{return"url"===e.type||"folder"===e.type||Y(e.url)||"string"==typeof e.name}catch{return!1}}(e))throw new f("EINVAL",`${e} is not a valid Bookmark entry in any known format.`);t=e;let{id:r,name:n,url:i,type:s,date_added:d,date_last_used:l,date_modified:u,guid:c,children:h}=t;if(o||s){if(o){if("url"!==o&&"folder"!==o)throw new f("EINVAL",`Only 'url' or 'folder' are supported types for strict-mode guard and normalizing of entries.Received strict:${o}.`);s=o}}else s=i&&!h?"url":"folder";const p={type:s,id:r||""+g.maxId[B()]++,guid:c||a.default.randomUUID(),date_added:d||F(Date.now()),date_modified:u||F(Date.now()),date_last_used:l||"0"};if("url"===s){if(p.url=i,p.name=n,!Y(i)&&"url"!==t.type)throw new f("EINVAL",`A bookmark must have a url field that is a valid URL. Received url: ${i} which was not invalid.`);if("string"!=typeof n)throw new f("EINVAL",`A bookmark must have a name field that is a string. Received name: ${i} which was not.`)}else{if("folder"!==s)throw new f("EINVAL",`Not a valid type of Bookmark entry ${s}`);if(p.name=n,p.date_modified=u||p.date_added,"string"!=typeof p.name)throw new f("EINVAL",`A bookmark folder must have a name field that is a string. Received name: ${i} which was not.`);p.children=[]}{const e=j(p.date_modified);if("[object Date]"!==Object.prototype.toString.call(e)||!isFinite(e))throw new f("EINVAL",`A bookmark entries date fields must be valid if supplied. Received date_modified: ${p.date_modified} which was not.`)}{const e=j(p.date_added);if("[object Date]"!==Object.prototype.toString.call(e)||!isFinite(e))throw new f("EINVAL",`A bookmark entries date fields must be valid if supplied. Received date_added: ${p.date_added} which was not.`)}return p}function j(e){return new Date(p+parseInt(e,10)/1e3)}function F(e){return(1e3*(new Date(e).getTime()-p)).toString()}function D(e){const o=a.default.createHash("md5"),{roots:t}=e;return i(t.bookmark_bar),i(t.other),i(t.synced),o.digest("hex");function r(e){o.update(e.id),o.update(Buffer.from(e.name,"utf16le")),o.update("url"),o.update(Buffer.from(e.url,"ascii"))}function n(e){o.update(e.id),o.update(Buffer.from(e.name,"utf16le")),o.update("folder");for(const o of e.children)i(o)}function i(e){({folder:n,url:r})[e.type](e)}}function C(e){V(e,X(e))}function V(e,o){o.checksum=D(o),l.default.writeFileSync(e,JSON.stringify(o,null,2))}function U(e,o={}){e=Array.from(e);const t=B();if(o.mountPoint=t,1===e.length&&Y(G(e))){const o=G(e);return t?g.books[t].get(o):Object.keys(g.books).filter((e=>0==g.active.size||g.active.has(e))).map((e=>g.books[e])).map((e=>e.get(o))).filter((e=>e))[0]}{!function(){if(!B())throw new TypeError("\n Bookmark file is not mounted. No fs-like API operations can be performed.\n You may:\n 1) Try setting a mount point with mount(), or \n 2) observing for bookmark changes with promisesWatch() \n (or, equivalently bookmarkChanges()) while you manually add, \n delete or alter your bookmarks from your browser, to try\n to automatically detect a valid mount point.\n ")}();const r=X(t);o.bookmarkObj=r;const{roots:n}=r;o.parents=[e[0]];let i=n[e.shift()];if(!i)throw new f("EINVAL",`Path must begin with a valid root node: ${Object.keys(n)}`);for(;i&&e.length;){const t=e.shift();o.parents.push(t),i=!e.length&&Y(t)?i.children.find((e=>"url"===e.type&&e.url===t)):i.children.find((e=>"folder"===e.type&&e.name===t))}if(!i||e.length)return;return i}}function G(e){return e[e.length-1]}function q(e){if(function(e){try{return z(JSON.parse(e))}catch{return!1}}(e))return JSON.parse(e);if(z(e))return e;if("string"==typeof e){if(/https?:/.test(e))throw new f("EINVAL","Sorry path shorthand ('/' separator syntax)\n can not be used with Bookmark URLs. \n Please use a path array instead.\n ");return console.log({path:e}),e.split("/").filter((e=>e.length))}throw new f("EINVAL",`Sorry path ${e} was not in a valid format. Please see the documentation at https://github.com/crisdosyago/bookmate`)}function J(e){if(Y(G(e=q(e))))throw new f("EINVAL",`Sorry, rmdir only works on folders not on bookmarks.\n Paths that end in URLs refer to bookmarks, those that\n end in plain strings refer to folders.\n You passed: ${e} which is a bookmarks path.\n `);return e}function W(e){if(!Y(G(e=q(e))))throw new f("EINVAL",`Sorry, unlink only works on bookmarks, not on folders. \n Paths that end in URLs refer to bookmarks, those that\n end in plain strings refer to folders.\n You passed: ${e} which is not a bookmark path.\n `);return e}function Y(e){try{return new URL(e),!0}catch{return!1}}function z(e){return Array.isArray(e)&&e.every((e=>"string"==typeof e))}function H(){const e=K(),o=l.default.readdirSync(e,{withFileTypes:!0}).reduce(((o,t)=>{if(t.isDirectory()&&S(t.name)){const r=d.default.resolve(e,t.name);l.default.existsSync(r)&&o.push(r)}return o}),[]);for(const e of o){const o=d.default.resolve(e,"Bookmarks");if(l.default.existsSync(o))return o}throw new TypeError("Could not find Bookmarks directory under standard chrome profile directory")}function K(){const e=s.default.platform();let o,t=k[e];if(h&&console.log({plat:e,name:t}),!t){if("win32"!==e)throw new TypeError(`Sorry! We don't know how to find the default Chrome profile on OS platform: ${e}`);{const e=s.default.release(),o=parseFloat(e);t=!Number.isNaN(o)&&o<=5.2?"winxp":"win"}}if(!y[t])throw new TypeError(`Sorry! We don't know how to find the default Chrome profile on OS name: ${t}`);return o=d.default.resolve(y[t].replace(/%([^%]+)%/g,(function(e,o){return process.env[o]}))),o}function Q(e,{toMap:o=!1,map:t,saved:r={}}={}){const n=[...Object.values(e.roots)],i=o?t||new Map:[],a=new Set,s=[];let d=0;for(;n.length;){const e=n.pop();let{id:r,name:l,type:u,url:c}=e;switch(r=parseInt(r),r>d&&(d=r),u){case"url":if(o){if(t)if(i.has(c)){const{name:e}=i.get(c);l!==e&&(a.has(c)||s.push({type:"Title updated",url:c,oldName:e,name:l}))}else s.push({type:"new",name:l,url:c});a.has(c)||i.set(c,e),a.add(c)}else i.push(e);break;case"folder":n.push(...e.children);break;default:console.info("New type",u,e)}}return t&&[...t.keys()].forEach((e=>{a.has(e)||(s.push({type:"delete",url:e}),t.delete(e))})),r.maxId=d,t?s:i}function X(e){let o;try{o=JSON.parse(l.default.readFileSync(e).toString())}catch(o){throw new TypeError(`Could not load Bookmark file ${e}, because: ${o+""}`)}return m.some((e=>o[e]))||console.warn(new TypeError(`Bookmark file ${e} does not have the structure we expect.`)),D(o),o}exports.bookmarkChanges=N,exports.default=E,exports.existsSync=L,exports.getProfileRootDir=K,exports.mkdirSync=P,exports.mount=A,exports.promisesWatch=O,exports.readFileSync=T,exports.readdirSync=R,exports.saveWithChecksum=C,exports.tryToFindBookmarksLocation=H,exports.unmount=$,exports.writeFileSync=M;