six-degrees 0.2.0 → 0.3.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +10 -0
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +39 -11
- package/.next/standalone/.next/required-server-files.json +4 -1
- package/.next/standalone/.next/routes-manifest.json +60 -0
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +11 -11
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +9 -9
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/admin-delete/route.js +3 -2
- package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/admin-update/route.js +3 -2
- package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bridges/route.js +3 -2
- package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bulk-import/route.js +3 -2
- package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js +9 -0
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/company-scores/route.js +3 -2
- package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js +3 -2
- package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/export/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/data/export/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/data/export/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/data/export/route.js +7 -0
- package/.next/standalone/.next/server/app/api/data/export/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/data/export/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/data/import/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/data/import/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/data/import/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/data/import/route.js +8 -0
- package/.next/standalone/.next/server/app/api/data/import/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/data/import/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/data/restart/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/data/restart/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/data/restart/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/data/restart/route.js +8 -0
- package/.next/standalone/.next/server/app/api/data/restart/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/data/restart/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/data/reveal/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/data/reveal/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/data/reveal/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/data/reveal/route.js +7 -0
- package/.next/standalone/.next/server/app/api/data/reveal/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/data/reveal/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/data/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/data/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/data/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/data/route.js +7 -0
- package/.next/standalone/.next/server/app/api/data/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/data/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js +3 -2
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js +2 -2
- package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js +3 -2
- package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js +3 -2
- package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/outreach/route.js +3 -2
- package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js +3 -2
- package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/scraper/route.js +3 -2
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/settings/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/settings/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/settings/route.js +9 -0
- package/.next/standalone/.next/server/app/api/settings/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/settings/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js +8 -0
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js +8 -0
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/setup-profile/route.js +3 -2
- package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js +3 -2
- package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update/route.js +1 -1
- package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update-images/route.js +3 -2
- package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js +3 -2
- package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/avatars/[file]/route.js +2 -1
- package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/favicon.ico/route.js +2 -1
- package/.next/standalone/.next/server/app/favicon.ico/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/import.html +1 -1
- package/.next/standalone/.next/server/app/import.rsc +13 -13
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/launch.html +2 -2
- package/.next/standalone/.next/server/app/launch.rsc +13 -13
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths.html +1 -1
- package/.next/standalone/.next/server/app/paths.rsc +13 -13
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/profile.html +1 -1
- package/.next/standalone/.next/server/app/profile.rsc +13 -13
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/queue.html +1 -1
- package/.next/standalone/.next/server/app/queue.rsc +13 -13
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/settings/page/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/settings/page/build-manifest.json +20 -0
- package/.next/standalone/.next/server/app/settings/page/next-font-manifest.json +11 -0
- package/.next/standalone/.next/server/app/settings/page/react-loadable-manifest.json +1 -0
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/settings/page.js +13 -0
- package/.next/standalone/.next/server/app/settings/page.js.map +5 -0
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/settings.html +1 -0
- package/.next/standalone/.next/server/app/settings.meta +26 -0
- package/.next/standalone/.next/server/app/settings.rsc +24 -0
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +24 -0
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +4 -0
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +28 -0
- package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup.html +1 -1
- package/.next/standalone/.next/server/app/setup.rsc +13 -13
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +13 -13
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +11 -11
- package/.next/standalone/.next/server/app-paths-manifest.json +10 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-rnla2._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__00la_y0._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__07t47cm._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cj5jqc._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e0fei3._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0i5hdu4._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljcioj._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ljnyho._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o5-tcg._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qrnous._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t3v1a1._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0uag-k5._.js +8 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0um1uey._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wmexuc._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xu34sz._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__133knx1._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__14ou7j5._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__15-17hl._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__16iu0hv._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__19b7mig._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1bbcwub._.js +8 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1eris29._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1gjo_cq._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1kbl21f._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1qcm08_._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1rphsdk._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1t3rugi._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1w25q0m._.js +3 -0
- package/.next/standalone/.next/server/chunks/_072jl0y._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0lw8_i0._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +21 -3
- package/.next/standalone/.next/server/chunks/_1fn30_l._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +2 -1
- package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1mxn_lb._.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_company-scores_legacy_route_actions_1ncspu_.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_data_export_route_actions_1lkimty.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_data_import_route_actions_1eji0kd.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_data_restart_route_actions_00_-u6o.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_data_reveal_route_actions_0-moqtn.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_data_route_actions_0j0w5ep.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_settings_route_actions_1bpm9ac.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_settings_sector-preview_route_actions_0w79454.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_settings_sector-suggestions_route_actions_1eoqugp.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +155 -0
- package/.next/standalone/.next/server/chunks/{lib_rpc_0mc73lk.js → lib_rpc_00tx2sm.js} +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_02-qa4d.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_02ndsg2.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_08q5vp6.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0_8i3ft.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -2
- package/.next/standalone/.next/server/chunks/lib_rpc_0huvw-3.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0ip2vdh.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0jzisgh.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0yi9xvd.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1jyhd0o.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1kn59-i.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1q2jkza.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1vpm1sk.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1x6p_31.js +3 -0
- package/.next/standalone/.next/server/chunks/node_modules_next_1zc5q0a._.js +158 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0mocm6d._.js → [root-of-the-server]__0an6blh._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h1_f_2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m0255u._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0bipr5s._.js → [root-of-the-server]__1z1j3ek._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_07u8byv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_08r_mlf._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_0ggabjc._.js → _0942v8n._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1djpk8t._.js → _0hl_t7r._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_0tb83dw._.js +132 -0
- package/.next/standalone/.next/server/chunks/ssr/_1-87bs1._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_settings_page_actions_11x4dq5.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_import_page_1fmu6_l.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_launch_page_19t0dyd.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +4 -4
- package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +5 -5
- package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules_1f8t_rp._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_1nkgb45._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_01xbj_9._.js → node_modules_1wax83z._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_00nap8h._.js → node_modules_next_dist_1bunoni._.js} +2 -2
- package/.next/standalone/.next/server/edge/chunks/[root-of-the-server]__0pco7a6._.js +2 -2
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +15 -7
- package/.next/standalone/.next/server/next-font-manifest.js +1 -1
- package/.next/standalone/.next/server/next-font-manifest.json +4 -0
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/prefetch-hints.json +14 -0
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/06x3jiiz96m5t.js +1 -0
- package/.next/{static/chunks/34at5d2ifl62i.js → standalone/.next/static/chunks/0ngx9s6-zgshe.js} +5 -5
- package/.next/standalone/.next/static/chunks/0q-_khndfxubv.js +2 -0
- package/.next/standalone/.next/static/chunks/1c79veqedvubt.js +1 -0
- package/.next/standalone/.next/static/chunks/1chp3nx45meg5.js +44 -0
- package/.next/standalone/.next/static/chunks/1irqj0lf-fu3d.js +1 -0
- package/.next/standalone/.next/static/chunks/1uadqylrod5_v.js +1 -0
- package/.next/standalone/.next/static/chunks/23u0zclgwpc4d.js +1 -0
- package/.next/standalone/.next/static/chunks/289ktj97di6zg.js +1 -0
- package/.next/standalone/.next/static/chunks/2b3lmf8e6uj8m.js +16 -0
- package/.next/standalone/.next/static/chunks/2jz311xl1pp_8.js +1 -0
- package/.next/standalone/.next/static/chunks/2ot61qsv6_0eq.js +1 -0
- package/.next/standalone/.next/static/chunks/3npq5yd7c4dmn.js +2 -0
- package/.next/standalone/.next/static/frfHFnRYTA0Wjrw8oZJtY/_clientMiddlewareManifest.js +14 -0
- package/.next/standalone/AGENTS.md +10 -0
- package/.next/standalone/CHANGELOG.md +325 -0
- package/.next/standalone/CONTRIBUTING.md +14 -2
- package/.next/standalone/README.md +181 -53
- package/.next/standalone/SECURITY.md +120 -10
- package/.next/standalone/app/api/company-scores/legacy/route.js +32 -0
- package/.next/standalone/app/api/company-scores/route.js +20 -11
- package/.next/standalone/app/api/data/export/route.js +49 -0
- package/.next/standalone/app/api/data/import/route.js +83 -0
- package/.next/standalone/app/api/data/restart/route.js +37 -0
- package/.next/standalone/app/api/data/reveal/route.js +14 -0
- package/.next/standalone/app/api/data/route.js +39 -0
- package/.next/standalone/app/api/ingest/route.js +15 -47
- package/.next/standalone/app/api/scraper/route.js +268 -128
- package/.next/standalone/app/api/settings/route.js +58 -0
- package/.next/standalone/app/api/settings/sector-preview/route.js +38 -0
- package/.next/standalone/app/api/settings/sector-suggestions/route.js +20 -0
- package/.next/standalone/app/api/update/route.js +197 -24
- package/.next/standalone/app/avatars/[file]/route.js +4 -2
- package/.next/standalone/app/components/CompanyScores.js +28 -7
- package/.next/standalone/app/components/EmptyState.js +8 -0
- package/.next/standalone/app/components/ForceGraph.js +40 -36
- package/.next/standalone/app/components/LegacyScoresCard.js +129 -0
- package/.next/standalone/app/components/Sidebar.js +19 -18
- package/.next/standalone/app/components/UpdatePanel.js +317 -99
- package/.next/standalone/app/components/UserProvider.js +4 -3
- package/.next/standalone/app/components/settings/DataSection.js +437 -0
- package/.next/standalone/app/components/settings/SectorSection.js +445 -0
- package/.next/standalone/app/components/ui.js +62 -0
- package/.next/standalone/app/import/page.js +3 -3
- package/.next/standalone/app/launch/page.js +30 -20
- package/.next/standalone/app/layout.js +2 -2
- package/.next/standalone/app/page.js +15 -2
- package/.next/standalone/app/paths/page.js +13 -5
- package/.next/standalone/app/profile/page.js +41 -186
- package/.next/standalone/app/queue/page.js +8 -2
- package/.next/standalone/app/settings/page.js +96 -0
- package/.next/standalone/app/setup/page.js +35 -19
- package/.next/standalone/bin/args.mjs +44 -0
- package/.next/standalone/bin/six-degrees.mjs +16 -17
- package/.next/standalone/install.sh +1 -1
- package/.next/standalone/lib/companies.js +44 -119
- package/.next/standalone/lib/csv.js +61 -27
- package/.next/standalone/lib/data-export.js +170 -0
- package/.next/standalone/lib/data-folder.js +238 -0
- package/.next/standalone/lib/data-import.js +878 -0
- package/.next/standalone/lib/db-client.js +48 -3
- package/.next/standalone/lib/durable.js +73 -0
- package/.next/standalone/lib/gate.js +90 -1
- package/.next/standalone/lib/ingest.js +40 -0
- package/.next/standalone/lib/legacy-offer.js +185 -0
- package/.next/standalone/lib/legacy-scores.js +77 -0
- package/.next/standalone/lib/linkedin-limits.js +124 -1
- package/.next/standalone/lib/release.js +5 -2
- package/.next/standalone/lib/rpc.js +121 -13
- package/.next/standalone/lib/sample-stats.js +16 -0
- package/.next/standalone/lib/scan-progress.js +13 -0
- package/.next/standalone/lib/scan-state.js +53 -0
- package/.next/standalone/lib/scanner-python.js +685 -0
- package/.next/standalone/lib/scanner-setup.js +104 -0
- package/.next/standalone/lib/scoring.js +747 -138
- package/.next/standalone/lib/scraper-client.js +10 -6
- package/.next/standalone/lib/sector-directory.js +1532 -0
- package/.next/standalone/lib/sector-focus.js +163 -0
- package/.next/standalone/lib/sector-labels.js +81 -0
- package/.next/standalone/lib/settings-effects.js +59 -0
- package/.next/standalone/lib/settings.js +83 -0
- package/.next/standalone/lib/updater-job.js +838 -0
- package/.next/standalone/lib/updater.js +494 -0
- package/.next/standalone/middleware.js +25 -28
- package/.next/standalone/next.config.mjs +19 -3
- package/.next/standalone/package-lock.json +103 -9
- package/.next/standalone/package.json +12 -11
- package/.next/standalone/public/demo-data.json +1 -1
- package/.next/standalone/scripts/apply-update.sh +482 -0
- package/.next/standalone/scripts/build-app.mjs +251 -9
- package/.next/standalone/scripts/gen-synthetic.mjs +16 -1
- package/.next/standalone/scripts/readme_buttons.py +89 -0
- package/.next/standalone/scripts/requirements.txt +106 -4
- package/.next/standalone/scripts/screenshots.py +2 -0
- package/.next/standalone/scripts/test-release-server.mjs +143 -0
- package/.next/standalone/server.js +1 -1
- package/.next/static/chunks/06x3jiiz96m5t.js +1 -0
- package/.next/{standalone/.next/static/chunks/34at5d2ifl62i.js → static/chunks/0ngx9s6-zgshe.js} +5 -5
- package/.next/static/chunks/0q-_khndfxubv.js +2 -0
- package/.next/static/chunks/1c79veqedvubt.js +1 -0
- package/.next/static/chunks/1chp3nx45meg5.js +44 -0
- package/.next/static/chunks/1irqj0lf-fu3d.js +1 -0
- package/.next/static/chunks/1uadqylrod5_v.js +1 -0
- package/.next/static/chunks/23u0zclgwpc4d.js +1 -0
- package/.next/static/chunks/289ktj97di6zg.js +1 -0
- package/.next/static/chunks/2b3lmf8e6uj8m.js +16 -0
- package/.next/static/chunks/2jz311xl1pp_8.js +1 -0
- package/.next/static/chunks/2ot61qsv6_0eq.js +1 -0
- package/.next/static/chunks/3npq5yd7c4dmn.js +2 -0
- package/.next/static/frfHFnRYTA0Wjrw8oZJtY/_clientMiddlewareManifest.js +14 -0
- package/CHANGELOG.md +325 -0
- package/README.md +181 -53
- package/SECURITY.md +120 -10
- package/bin/args.mjs +44 -0
- package/bin/six-degrees.mjs +16 -17
- package/lib/companies.js +44 -119
- package/lib/csv.js +61 -27
- package/lib/data-export.js +170 -0
- package/lib/data-folder.js +238 -0
- package/lib/data-import.js +878 -0
- package/lib/db-client.js +48 -3
- package/lib/durable.js +73 -0
- package/lib/gate.js +90 -1
- package/lib/ingest.js +40 -0
- package/lib/legacy-offer.js +185 -0
- package/lib/legacy-scores.js +77 -0
- package/lib/linkedin-limits.js +124 -1
- package/lib/release.js +5 -2
- package/lib/rpc.js +121 -13
- package/lib/sample-stats.js +16 -0
- package/lib/scan-progress.js +13 -0
- package/lib/scan-state.js +53 -0
- package/lib/scanner-python.js +685 -0
- package/lib/scanner-setup.js +104 -0
- package/lib/scoring.js +747 -138
- package/lib/scraper-client.js +10 -6
- package/lib/sector-directory.js +1532 -0
- package/lib/sector-focus.js +163 -0
- package/lib/sector-labels.js +81 -0
- package/lib/settings-effects.js +59 -0
- package/lib/settings.js +83 -0
- package/lib/updater-job.js +838 -0
- package/lib/updater.js +494 -0
- package/package.json +12 -11
- package/public/demo-data.json +1 -1
- package/scripts/requirements.txt +106 -4
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jrmmj-._.js +0 -150
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qx548g._.js +0 -151
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1an3leo._.js +0 -150
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1m8zedk._.js +0 -155
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1zor0ys._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0_rp-01._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0hzdk79._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0ow7u5r._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0t0zc2d._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0wa631d._.js +0 -3
- package/.next/standalone/.next/server/chunks/_13ccnao._.js +0 -3
- package/.next/standalone/.next/server/chunks/_13es063._.js +0 -3
- package/.next/standalone/.next/server/chunks/_142872j._.js +0 -3
- package/.next/standalone/.next/server/chunks/_18wsb_a._.js +0 -8
- package/.next/standalone/.next/server/chunks/_1e-4h5h._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1gvvd7n._.js +0 -158
- package/.next/standalone/.next/server/chunks/_1moda66._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1p6ghzv._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1xmmi_v._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1z-yrlf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0bf3x4q._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +0 -4
- package/.next/standalone/.next/static/N9wokeIiC9rUIGmzBhj6T/_clientMiddlewareManifest.js +0 -6
- package/.next/standalone/.next/static/chunks/1odow92u6wxz0.js +0 -1
- package/.next/standalone/.next/static/chunks/1wb_is4wsdm-u.js +0 -1
- package/.next/standalone/.next/static/chunks/1zmpcmngg5c7-.js +0 -2
- package/.next/standalone/.next/static/chunks/220-ne82aus0c.js +0 -44
- package/.next/standalone/.next/static/chunks/2rrf5kzx2fhin.js +0 -2
- package/.next/standalone/.next/static/chunks/2s_k9x4d0gi9h.js +0 -16
- package/.next/standalone/.next/static/chunks/3hsi90cejq2dg.js +0 -1
- package/.next/standalone/.next/static/chunks/3pfyuezyqd9ob.js +0 -1
- package/.next/standalone/.next/static/chunks/3ts-_4i6bfkh2.js +0 -1
- package/.next/standalone/scripts/score-connections.sql +0 -80
- package/.next/standalone/scripts/score_new_connections.sql +0 -77
- package/.next/static/N9wokeIiC9rUIGmzBhj6T/_clientMiddlewareManifest.js +0 -6
- package/.next/static/chunks/1odow92u6wxz0.js +0 -1
- package/.next/static/chunks/1wb_is4wsdm-u.js +0 -1
- package/.next/static/chunks/1zmpcmngg5c7-.js +0 -2
- package/.next/static/chunks/220-ne82aus0c.js +0 -44
- package/.next/static/chunks/2rrf5kzx2fhin.js +0 -2
- package/.next/static/chunks/2s_k9x4d0gi9h.js +0 -16
- package/.next/static/chunks/3hsi90cejq2dg.js +0 -1
- package/.next/static/chunks/3pfyuezyqd9ob.js +0 -1
- package/.next/static/chunks/3ts-_4i6bfkh2.js +0 -1
- package/scripts/score_new_connections.sql +0 -77
- /package/.next/standalone/.next/static/{N9wokeIiC9rUIGmzBhj6T → frfHFnRYTA0Wjrw8oZJtY}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{N9wokeIiC9rUIGmzBhj6T → frfHFnRYTA0Wjrw8oZJtY}/_ssgManifest.js +0 -0
- /package/.next/static/{N9wokeIiC9rUIGmzBhj6T → frfHFnRYTA0Wjrw8oZJtY}/_buildManifest.js +0 -0
- /package/.next/static/{N9wokeIiC9rUIGmzBhj6T → frfHFnRYTA0Wjrw8oZJtY}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
// Bringing a network in from another computer: check the file, stage it, and
|
|
2
|
+
// swap it in at the next start.
|
|
3
|
+
//
|
|
4
|
+
// Replace, never merge. Merging two networks means deciding again who "you"
|
|
5
|
+
// are, re-parenting every circle (a bridge's id is a UUID made on one
|
|
6
|
+
// computer), and de-duplicating against the unique index. Each of those is a
|
|
7
|
+
// way to corrupt data silently (TRAPS §7, §25, §32). Moving to a new computer
|
|
8
|
+
// is a replacement anyway, and what was here is kept in backups/ first.
|
|
9
|
+
//
|
|
10
|
+
// Applied at the next start, not live. The database handle is opened once and
|
|
11
|
+
// kept (lib/db-client.js), and nothing can safely close it under a running
|
|
12
|
+
// server. So the import route only checks the file and stages it in
|
|
13
|
+
// import-pending/; getDb() finishes the job the next time the app starts,
|
|
14
|
+
// before it opens the database.
|
|
15
|
+
//
|
|
16
|
+
// The file is untrusted until it has been checked (SECURITY.md): it came from
|
|
17
|
+
// somewhere else. Nothing in the data folder changes until every check passes,
|
|
18
|
+
// and the database it brings is rebuilt into this version's own schema, so
|
|
19
|
+
// only rows travel, never table definitions.
|
|
20
|
+
|
|
21
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
22
|
+
import {
|
|
23
|
+
openSync, readSync, closeSync, readFileSync, writeFileSync, renameSync, rmSync, mkdirSync,
|
|
24
|
+
lstatSync, cpSync, copyFileSync,
|
|
25
|
+
} from 'node:fs';
|
|
26
|
+
import { open } from 'node:fs/promises';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import { compareVersions } from './release.js';
|
|
29
|
+
import { requestRefusal } from './gate.js';
|
|
30
|
+
import { runningNow } from './scan-state.js';
|
|
31
|
+
import { durable } from './durable.js';
|
|
32
|
+
import { budgetFileProblem, mergeBudgetFiles } from './linkedin-limits.js';
|
|
33
|
+
import {
|
|
34
|
+
AVATAR_FILE, TRAVELLING_FILES, NETWORK_FILES, BUDGET_FILES, PENDING_DIR, IMPORT_BACKUP_PREFIX,
|
|
35
|
+
sweepLeftovers, namesInFolder,
|
|
36
|
+
} from './data-folder.js';
|
|
37
|
+
import {
|
|
38
|
+
EXPORT_FORMAT, EXPORT_TABLES, sha256, sqlString, appTables, appIndexes, countPeople,
|
|
39
|
+
} from './data-export.js';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The biggest file an import accepts. The body is written to disk as it
|
|
43
|
+
* arrives (receiveUpload), never held in memory, so this bounds the disk space
|
|
44
|
+
* an upload can take. A network of 20,000 people with their photos (2-4 KB
|
|
45
|
+
* each) is well under 100 MB.
|
|
46
|
+
*/
|
|
47
|
+
export const MAX_IMPORT_BYTES = 256 * 1024 * 1024;
|
|
48
|
+
|
|
49
|
+
/** The route that takes an upload. middleware.js leaves it alone; admitImport makes its checks instead. */
|
|
50
|
+
export const IMPORT_ROUTE = '/api/data/import';
|
|
51
|
+
|
|
52
|
+
const SQLITE_MAGIC = Buffer.from('SQLite format 3\0', 'latin1');
|
|
53
|
+
|
|
54
|
+
/** A refusal whose message is fit to show as it is. Nothing was changed. */
|
|
55
|
+
export class ImportError extends Error {
|
|
56
|
+
constructor(message, { status = 400, ...extra } = {}) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.status = status;
|
|
59
|
+
this.extra = extra;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function megabytes(bytes) {
|
|
64
|
+
const n = bytes / 1024 / 1024;
|
|
65
|
+
return `${n < 10 ? n.toFixed(1) : Math.round(n)} MB`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Names from the file are shown in messages; keep a hostile one short.
|
|
69
|
+
const shortName = (s) => String(s).slice(0, 80);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The checks that need no file: may an import start at all?
|
|
73
|
+
* `scanRunning` is what the scanner's route runs (lib/scan-state.js
|
|
74
|
+
* scannerJob: 'full', 'install', …), or null; the refusal names it.
|
|
75
|
+
* @returns an ImportError to refuse with, or null to go ahead.
|
|
76
|
+
*/
|
|
77
|
+
export function importPreflight({ scanRunning, pending, declared, currentPeople = 0, confirmedPeople, max = MAX_IMPORT_BYTES }) {
|
|
78
|
+
if (scanRunning) {
|
|
79
|
+
return new ImportError(`${runningNow(scanRunning)}. Let it finish, or stop it on the Scan page, then import.`, { status: 409 });
|
|
80
|
+
}
|
|
81
|
+
if (pending) {
|
|
82
|
+
return new ImportError('An import is already waiting to finish. Restart Six Degrees to finish it, or cancel it first.', { status: 409 });
|
|
83
|
+
}
|
|
84
|
+
if (!Number.isSafeInteger(declared) || declared <= 0) {
|
|
85
|
+
return new ImportError('Choose a Six Degrees export (a .sixdegrees file) to import.');
|
|
86
|
+
}
|
|
87
|
+
if (declared > max) {
|
|
88
|
+
return new ImportError(`That file is ${megabytes(declared)}. This copy can import files up to ${megabytes(max)}.`, { status: 413 });
|
|
89
|
+
}
|
|
90
|
+
// Replacing a network needs a yes that names what it replaces. If a scan has
|
|
91
|
+
// added people since the page asked, the count no longer matches: ask again.
|
|
92
|
+
if (currentPeople > 0 && confirmedPeople !== currentPeople) {
|
|
93
|
+
return new ImportError(
|
|
94
|
+
`This copy already has ${currentPeople.toLocaleString('en-US')} people. Confirm that the import replaces them.`,
|
|
95
|
+
{ status: 409, needsConfirm: true, people: currentPeople },
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Everything the import route decides before it reads a byte of the body.
|
|
103
|
+
*
|
|
104
|
+
* middleware.js leaves this one route alone (its matcher), because Next hands
|
|
105
|
+
* a request to middleware only after copying its body into memory, the whole
|
|
106
|
+
* of it up to the proxy's limit, even when middleware then refuses it. So the
|
|
107
|
+
* checks middleware.js makes everywhere else come first here (requestRefusal,
|
|
108
|
+
* the same function), then importPreflight. The facts about this copy are
|
|
109
|
+
* asked for only once a request has passed the gate, so a refused one costs
|
|
110
|
+
* nothing, and request.body is never touched.
|
|
111
|
+
*
|
|
112
|
+
* @returns {{ refused: { status, error, ... } } | { declared: number, currentPeople: number }}
|
|
113
|
+
*/
|
|
114
|
+
export function admitImport(request, { env, scanRunning, pending, currentPeople, max = MAX_IMPORT_BYTES }) {
|
|
115
|
+
const gate = requestRefusal({ method: request.method, pathname: IMPORT_ROUTE, headers: request.headers, env });
|
|
116
|
+
if (gate) return { refused: gate };
|
|
117
|
+
const declared = Number(request.headers.get('x-six-degrees-size'));
|
|
118
|
+
const people = currentPeople();
|
|
119
|
+
const refusal = importPreflight({
|
|
120
|
+
scanRunning: scanRunning(),
|
|
121
|
+
pending: pending(),
|
|
122
|
+
declared,
|
|
123
|
+
currentPeople: people,
|
|
124
|
+
confirmedPeople: Number(request.headers.get('x-six-degrees-replace')),
|
|
125
|
+
max,
|
|
126
|
+
});
|
|
127
|
+
if (refusal) return { refused: { status: refusal.status, error: refusal.message, ...refusal.extra } };
|
|
128
|
+
return { declared, currentPeople: people };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Write a request body to `file`, refusing more than `declared` bytes and
|
|
133
|
+
* anything short of it. Resolves the number of bytes written.
|
|
134
|
+
*/
|
|
135
|
+
export async function receiveUpload(body, file, { declared }) {
|
|
136
|
+
const fh = await open(file, 'wx', 0o600);
|
|
137
|
+
let received = 0;
|
|
138
|
+
try {
|
|
139
|
+
if (body) {
|
|
140
|
+
for await (const chunk of body) {
|
|
141
|
+
const bytes = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
|
|
142
|
+
received += bytes.byteLength;
|
|
143
|
+
if (received > declared) {
|
|
144
|
+
throw new ImportError('The file was bigger than the size it was sent with, so nothing was imported. Try again.');
|
|
145
|
+
}
|
|
146
|
+
for (let at = 0; at < bytes.byteLength;) {
|
|
147
|
+
const { bytesWritten } = await fh.write(bytes, at, bytes.byteLength - at);
|
|
148
|
+
at += bytesWritten;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} finally {
|
|
153
|
+
await fh.close();
|
|
154
|
+
}
|
|
155
|
+
if (received !== declared) {
|
|
156
|
+
throw new ImportError(`The file didn't arrive whole (${received} of ${declared} bytes), so nothing was imported. Try again.`);
|
|
157
|
+
}
|
|
158
|
+
return received;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 'photo', 'file' (one of the scanner's travelling files), or null for anything an export never carries. */
|
|
162
|
+
export function exportedFileKind(rel) {
|
|
163
|
+
if (typeof rel !== 'string') return null;
|
|
164
|
+
if (TRAVELLING_FILES.includes(rel)) return 'file';
|
|
165
|
+
// The same pattern the /avatars route serves: no folders, no dots but the
|
|
166
|
+
// extension, so no path can climb out of avatars/.
|
|
167
|
+
if (rel.startsWith('avatars/') && AVATAR_FILE.test(rel.slice('avatars/'.length))) return 'photo';
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Tables every export must have. The rest of this version's tables are
|
|
173
|
+
* optional: an export made before a table was added simply lacks it, and it is
|
|
174
|
+
* made empty here.
|
|
175
|
+
*/
|
|
176
|
+
const REQUIRED_TABLES = ['users', 'linkedin_connections', ...Object.keys(EXPORT_TABLES)];
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Names this version no longer makes: tables, indexes, and columns by table.
|
|
180
|
+
* An export made before one was dropped or renamed still has it, and without
|
|
181
|
+
* its old name here that export would be refused as "something an export never
|
|
182
|
+
* holds". Listed, it is accepted and left behind: its rows or values are not
|
|
183
|
+
* carried into this version (a renamed column's values would need copying
|
|
184
|
+
* across by hand in stageImport).
|
|
185
|
+
*
|
|
186
|
+
* Empty today. Whoever drops or renames a table, index or column adds its old
|
|
187
|
+
* name here in the same change (docs/brain/SCHEMA.md).
|
|
188
|
+
*/
|
|
189
|
+
export const RETIRED = Object.freeze({ tables: [], indexes: [], columns: {} });
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The tables, columns and indexes this version makes, to hold a file up against.
|
|
193
|
+
* A column is required only if a row can't be stored without it (a key, or NOT
|
|
194
|
+
* NULL with no default). Any other column may be missing, as in an export from
|
|
195
|
+
* before it was added, and gets its default. So adding a column or a table
|
|
196
|
+
* later never turns away the exports people already have.
|
|
197
|
+
*/
|
|
198
|
+
function referenceSchema(applySchema, schemaSql) {
|
|
199
|
+
const ref = new DatabaseSync(':memory:');
|
|
200
|
+
try {
|
|
201
|
+
applySchema(ref);
|
|
202
|
+
ref.exec(`${Object.values(EXPORT_TABLES).join(';\n')};`);
|
|
203
|
+
const tables = new Map();
|
|
204
|
+
for (const t of [...appTables(schemaSql), ...Object.keys(EXPORT_TABLES)]) {
|
|
205
|
+
const cols = ref.prepare(`PRAGMA table_info("${t}")`).all();
|
|
206
|
+
tables.set(t, {
|
|
207
|
+
required: cols.filter((c) => c.pk > 0 || (c.notnull && c.dflt_value === null)).map((c) => c.name),
|
|
208
|
+
allowed: new Set(cols.map((c) => c.name)),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return { tables, indexes: new Set(appIndexes(schemaSql)) };
|
|
212
|
+
} finally {
|
|
213
|
+
ref.close();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Everything about the file that can be checked without changing anything.
|
|
219
|
+
* @throws ImportError with a message fit to show.
|
|
220
|
+
* @returns {{ fromVersion, exportedAt, people, photos, files }}
|
|
221
|
+
*/
|
|
222
|
+
export function validateImport(file, { applySchema, schemaSql, appVersion, retired = RETIRED }) {
|
|
223
|
+
const head = Buffer.alloc(100);
|
|
224
|
+
let got = 0;
|
|
225
|
+
const fd = openSync(file, 'r');
|
|
226
|
+
try { got = readSync(fd, head, 0, 100, 0); } finally { closeSync(fd); }
|
|
227
|
+
if (got < 100 || !head.subarray(0, 16).equals(SQLITE_MAGIC)) {
|
|
228
|
+
throw new ImportError("This isn't a Six Degrees export (a .sixdegrees file), so nothing was imported.");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let db;
|
|
232
|
+
try {
|
|
233
|
+
db = new DatabaseSync(file, { readOnly: true });
|
|
234
|
+
} catch (err) {
|
|
235
|
+
throw new ImportError(`This file couldn't be opened (${err.message}), so nothing was imported.`);
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
// Its schema was written somewhere else: nothing in it may call functions,
|
|
239
|
+
// and SQLite checks each page's cells as it reads them. Both are SQLite's
|
|
240
|
+
// own advice for a database file from somewhere else.
|
|
241
|
+
db.exec('PRAGMA trusted_schema = OFF; PRAGMA query_only = ON; PRAGMA cell_size_check = ON;');
|
|
242
|
+
|
|
243
|
+
let check;
|
|
244
|
+
try { check = db.prepare('PRAGMA integrity_check').all(); } catch (err) { check = [{ integrity_check: err.message }]; }
|
|
245
|
+
if (check.length !== 1 || check[0].integrity_check !== 'ok') {
|
|
246
|
+
throw new ImportError("This file is damaged (its database fails SQLite's own check), so nothing was imported. Export it again on the other computer.");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const ref = referenceSchema(applySchema, schemaSql);
|
|
250
|
+
const notOurs = (type, name) => new ImportError(
|
|
251
|
+
`This file holds something a Six Degrees export never does (the ${shortName(type)} “${shortName(name)}”), so nothing was imported.`,
|
|
252
|
+
);
|
|
253
|
+
// Only the app's own tables and indexes, by name, and each table an
|
|
254
|
+
// ordinary one: a virtual table can carry an app table's name while a
|
|
255
|
+
// module of its own answers for it. A view, a trigger or anything else the
|
|
256
|
+
// app never makes is refused whatever it is.
|
|
257
|
+
const kinds = new Map(db.prepare('PRAGMA main.table_list').all().map((r) => [r.name, r]));
|
|
258
|
+
const objects = db.prepare('SELECT type, name, tbl_name FROM sqlite_master').all();
|
|
259
|
+
const knownTable = (name) => ref.tables.has(name) || retired.tables.includes(name);
|
|
260
|
+
for (const o of objects) {
|
|
261
|
+
if (o.type === 'table') {
|
|
262
|
+
const kind = kinds.get(o.name)?.type;
|
|
263
|
+
if (kind !== 'table') throw notOurs(`${kind || 'unknown'} table`, o.name);
|
|
264
|
+
if (knownTable(o.name)) continue;
|
|
265
|
+
}
|
|
266
|
+
if (o.type === 'index' && (ref.indexes.has(o.name) || retired.indexes.includes(o.name)
|
|
267
|
+
|| (String(o.name).startsWith('sqlite_autoindex_') && knownTable(o.tbl_name)))) continue;
|
|
268
|
+
throw notOurs(o.type, o.name);
|
|
269
|
+
}
|
|
270
|
+
const present = new Set(objects.filter((o) => o.type === 'table').map((o) => o.name));
|
|
271
|
+
if (!present.has('sd_export_manifest') || !present.has('sd_export_files')) {
|
|
272
|
+
throw new ImportError("This is a database but not a Six Degrees export, so nothing was imported. Make one on the other computer with Settings → Your data → Save a copy.");
|
|
273
|
+
}
|
|
274
|
+
for (const [t, { required, allowed }] of ref.tables) {
|
|
275
|
+
if (!present.has(t)) {
|
|
276
|
+
// Made empty when the network is rebuilt: an export from before this table existed.
|
|
277
|
+
if (!REQUIRED_TABLES.includes(t)) continue;
|
|
278
|
+
throw new ImportError(`This export has no “${t}” table, so nothing was imported.`);
|
|
279
|
+
}
|
|
280
|
+
// table_xinfo, not table_info: it also lists generated and hidden
|
|
281
|
+
// columns, which the app never makes.
|
|
282
|
+
const cols = db.prepare(`PRAGMA table_xinfo("${t}")`).all();
|
|
283
|
+
const names = cols.map((c) => c.name);
|
|
284
|
+
const oldNames = retired.columns[t] || [];
|
|
285
|
+
const laidOut = kinds.get(t).wr === 0
|
|
286
|
+
&& cols.every((c) => c.hidden === 0 && (allowed.has(c.name) || oldNames.includes(c.name)))
|
|
287
|
+
&& required.every((c) => names.includes(c));
|
|
288
|
+
if (!laidOut) {
|
|
289
|
+
throw new ImportError(`This export's “${t}” table isn't laid out the way Six Degrees makes it, so nothing was imported.`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const m = Object.fromEntries(db.prepare('SELECT key, value FROM sd_export_manifest').all().map((r) => [r.key, r.value]));
|
|
294
|
+
const format = Number(m.format);
|
|
295
|
+
if (format !== EXPORT_FORMAT) {
|
|
296
|
+
throw new ImportError(Number.isInteger(format) && format > EXPORT_FORMAT
|
|
297
|
+
? 'This export was made by a newer Six Degrees than this copy. Update this copy first, then import it.'
|
|
298
|
+
: "This file's export details aren't ones Six Degrees writes, so nothing was imported.");
|
|
299
|
+
}
|
|
300
|
+
const fromVersion = String(m.app_version ?? '');
|
|
301
|
+
if (!/^v?\d+\.\d+\.\d+/.test(fromVersion)) {
|
|
302
|
+
throw new ImportError("This file doesn't say which version of Six Degrees made it, so nothing was imported.");
|
|
303
|
+
}
|
|
304
|
+
// There is no way back down: an older copy can't know what a newer one
|
|
305
|
+
// changed, and its backups only ever guard an upgrade.
|
|
306
|
+
if (compareVersions(fromVersion, appVersion) > 0) {
|
|
307
|
+
throw new ImportError(`This file comes from Six Degrees ${shortName(fromVersion)}, which is newer than this copy (${appVersion}). Update this copy first, then import it.`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// The manifest counted every table and file as the export was made, so a
|
|
311
|
+
// file changed since then (rows or photos added or taken out) gives itself
|
|
312
|
+
// away here.
|
|
313
|
+
let counts = null;
|
|
314
|
+
try { counts = JSON.parse(m.counts); } catch { /* refused just below */ }
|
|
315
|
+
if (!counts || typeof counts !== 'object' || Array.isArray(counts)) {
|
|
316
|
+
throw new ImportError("This file's export details aren't ones Six Degrees writes, so nothing was imported.");
|
|
317
|
+
}
|
|
318
|
+
const changedSince = (what, has, says) => new ImportError(
|
|
319
|
+
`This file's ${what} doesn't match its own export details (${has}, where they say ${shortName(says)}), so nothing was imported. Export it again on the other computer.`,
|
|
320
|
+
);
|
|
321
|
+
for (const t of present) {
|
|
322
|
+
if (Object.hasOwn(EXPORT_TABLES, t)) continue;
|
|
323
|
+
const n = db.prepare(`SELECT count(*) AS n FROM "${t}"`).get().n;
|
|
324
|
+
if (!Object.hasOwn(counts, t) || Number(counts[t]) !== n) throw changedSince(`“${t}” table`, `${n} rows`, counts[t] ?? 'nothing');
|
|
325
|
+
}
|
|
326
|
+
// A table it counted rows in has to still be there. (One made after this
|
|
327
|
+
// export was isn't in its counts at all, and is made empty.)
|
|
328
|
+
for (const [t, n] of Object.entries(counts)) {
|
|
329
|
+
if (!present.has(t) && Number(n) !== 0) throw changedSince(`“${shortName(t)}” table`, 'no table', n);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
let photos = 0;
|
|
333
|
+
let files = 0;
|
|
334
|
+
const one = db.prepare('SELECT bytes, sha256 FROM sd_export_files WHERE path = ?');
|
|
335
|
+
for (const { path: rel } of db.prepare('SELECT path FROM sd_export_files').all()) {
|
|
336
|
+
const kind = exportedFileKind(rel);
|
|
337
|
+
if (!kind) {
|
|
338
|
+
throw new ImportError(`This file carries “${shortName(rel)}”, which an export never includes, so nothing was imported.`);
|
|
339
|
+
}
|
|
340
|
+
const row = one.get(rel);
|
|
341
|
+
if (!(row?.bytes instanceof Uint8Array) || sha256(row.bytes) !== row.sha256) {
|
|
342
|
+
throw new ImportError(`“${rel}” inside this file is damaged (it doesn't match its checksum), so nothing was imported.`);
|
|
343
|
+
}
|
|
344
|
+
if (kind === 'photo') { photos++; continue; }
|
|
345
|
+
let parsed;
|
|
346
|
+
try { parsed = JSON.parse(Buffer.from(row.bytes).toString('utf8')); } catch { parsed = undefined; }
|
|
347
|
+
// Every one of the scanner's files is a JSON object; the scanner and the
|
|
348
|
+
// app read them as one.
|
|
349
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
350
|
+
throw new ImportError(`“${rel}” inside this file can't be read, so nothing was imported.`);
|
|
351
|
+
}
|
|
352
|
+
// The LinkedIn budget reaches the scanner as it is: held to the same
|
|
353
|
+
// rules the app writes it by (a daily limit of 0 would mean no limit).
|
|
354
|
+
const problem = BUDGET_FILES.includes(rel) ? budgetFileProblem(rel, parsed) : null;
|
|
355
|
+
if (problem) {
|
|
356
|
+
throw new ImportError(`“${rel}” inside this file can't be used (${problem}), so nothing was imported.`);
|
|
357
|
+
}
|
|
358
|
+
files++;
|
|
359
|
+
}
|
|
360
|
+
if (Number(m.files) !== photos + files) {
|
|
361
|
+
throw changedSince('list of files', `${photos + files} files`, m.files ?? 'nothing');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return { fromVersion, exportedAt: m.exported_at || null, people: countPeople(db), photos, files };
|
|
365
|
+
} catch (err) {
|
|
366
|
+
// Anything SQLite itself trips on while reading a strange file is still a
|
|
367
|
+
// plain refusal, never a crash with nothing said.
|
|
368
|
+
if (err instanceof ImportError) throw err;
|
|
369
|
+
throw new ImportError(`This file couldn't be read as a Six Degrees export (${shortName(err.message)}), so nothing was imported.`);
|
|
370
|
+
} finally {
|
|
371
|
+
db.close();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Check the uploaded file and stage it for the next start. `upload` sits alone
|
|
377
|
+
* in a private working folder inside `dir` (the route made it with mkdtemp); on
|
|
378
|
+
* success that folder becomes import-pending/, in one rename.
|
|
379
|
+
* @returns what the Settings page shows while the import waits.
|
|
380
|
+
*/
|
|
381
|
+
export function stageImport(upload, {
|
|
382
|
+
dir, applySchema, schemaSql, appVersion, replacedPeople = 0, now = new Date(), retired = RETIRED,
|
|
383
|
+
}) {
|
|
384
|
+
const info = validateImport(upload, { applySchema, schemaSql, appVersion, retired });
|
|
385
|
+
const work = path.dirname(upload);
|
|
386
|
+
const filesDir = path.join(work, 'files');
|
|
387
|
+
const staged = path.join(work, 'data.sqlite');
|
|
388
|
+
const scannerFiles = [];
|
|
389
|
+
|
|
390
|
+
const db = new DatabaseSync(staged);
|
|
391
|
+
try {
|
|
392
|
+
db.exec('PRAGMA trusted_schema = OFF; PRAGMA cell_size_check = ON;');
|
|
393
|
+
// This version's own tables and indexes. Only rows come from the file.
|
|
394
|
+
applySchema(db);
|
|
395
|
+
db.exec(`ATTACH DATABASE ${sqlString(upload)} AS up`);
|
|
396
|
+
try {
|
|
397
|
+
db.exec('BEGIN');
|
|
398
|
+
for (const t of appTables(schemaSql)) {
|
|
399
|
+
// The columns both have. A column (or a whole table) added since the
|
|
400
|
+
// file was made is left to this version's default; one this version
|
|
401
|
+
// no longer has (RETIRED) is left behind.
|
|
402
|
+
const ours = new Set(db.prepare(`PRAGMA main.table_info("${t}")`).all().map((c) => c.name));
|
|
403
|
+
const cols = db.prepare(`PRAGMA up.table_info("${t}")`).all()
|
|
404
|
+
.filter((c) => ours.has(c.name)).map((c) => `"${c.name}"`).join(', ');
|
|
405
|
+
if (!cols) continue;
|
|
406
|
+
db.exec(`INSERT INTO main."${t}" (${cols}) SELECT ${cols} FROM up."${t}" ORDER BY rowid`);
|
|
407
|
+
}
|
|
408
|
+
db.exec('COMMIT');
|
|
409
|
+
} catch (err) {
|
|
410
|
+
try { db.exec('ROLLBACK'); } catch { /* nothing open */ }
|
|
411
|
+
throw new ImportError(`This export's network doesn't fit this copy's database (${err.message}), so nothing was imported.`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
mkdirSync(path.join(filesDir, 'avatars'), { recursive: true });
|
|
415
|
+
const one = db.prepare('SELECT bytes, sha256 FROM up.sd_export_files WHERE path = ?');
|
|
416
|
+
for (const { path: rel } of db.prepare('SELECT path FROM up.sd_export_files').all()) {
|
|
417
|
+
const row = one.get(rel);
|
|
418
|
+
// Checked above; checked again where the bytes are written.
|
|
419
|
+
const kind = exportedFileKind(rel);
|
|
420
|
+
if (!kind || !(row?.bytes instanceof Uint8Array) || sha256(row.bytes) !== row.sha256) {
|
|
421
|
+
throw new ImportError(`“${shortName(rel)}” inside this file is damaged, so nothing was imported.`);
|
|
422
|
+
}
|
|
423
|
+
writeFileSync(path.join(filesDir, rel), row.bytes, { flag: 'wx' });
|
|
424
|
+
if (kind === 'file') scannerFiles.push(path.join(filesDir, rel));
|
|
425
|
+
}
|
|
426
|
+
} finally {
|
|
427
|
+
db.close();
|
|
428
|
+
}
|
|
429
|
+
rmSync(upload, { force: true });
|
|
430
|
+
|
|
431
|
+
const ready = {
|
|
432
|
+
stagedAt: now.toISOString(),
|
|
433
|
+
exportedAt: info.exportedAt,
|
|
434
|
+
fromVersion: info.fromVersion,
|
|
435
|
+
people: info.people,
|
|
436
|
+
photos: info.photos,
|
|
437
|
+
replacedPeople,
|
|
438
|
+
};
|
|
439
|
+
writeFileSync(path.join(work, 'READY'), `${JSON.stringify(ready, null, 2)}\n`);
|
|
440
|
+
// On the disk before import-pending/ appears: the next start moves this
|
|
441
|
+
// network into place. The photos aren't synced one by one (each sync costs
|
|
442
|
+
// about 4 ms on a Mac, 20 s for 5,000 of them); a photo lost to a power cut
|
|
443
|
+
// in the next few seconds shows as initials until the next scan saves it.
|
|
444
|
+
for (const file of [staged, ...scannerFiles, path.join(work, 'READY')]) durable.syncFile(file);
|
|
445
|
+
for (const folder of [path.join(filesDir, 'avatars'), filesDir, work]) durable.syncFolder(folder);
|
|
446
|
+
// import-pending/ never exists half-made.
|
|
447
|
+
renameSync(work, path.join(dir, PENDING_DIR));
|
|
448
|
+
durable.syncFolder(dir);
|
|
449
|
+
return ready;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ── the next start ───────────────────────────────────────────────────────────
|
|
453
|
+
|
|
454
|
+
const exists = (p) => {
|
|
455
|
+
try { lstatSync(p); return true; } catch { return false; }
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
/** Empty (SQLite makes a new database of it), or starting with SQLite's own header. */
|
|
459
|
+
function looksLikeSqlite(file) {
|
|
460
|
+
const head = Buffer.alloc(SQLITE_MAGIC.length);
|
|
461
|
+
const fd = openSync(file, 'r');
|
|
462
|
+
try {
|
|
463
|
+
const got = readSync(fd, head, 0, head.length, 0);
|
|
464
|
+
return got === 0 || (got === head.length && head.equals(SQLITE_MAGIC));
|
|
465
|
+
} finally {
|
|
466
|
+
closeSync(fd);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function readJson(file) {
|
|
471
|
+
try { return JSON.parse(readFileSync(file, 'utf8')); } catch { return null; }
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* A rename, or a copy and delete when SIX_DEGREES_DB puts the database on
|
|
476
|
+
* another disk. A copied file is synced before its original goes.
|
|
477
|
+
*/
|
|
478
|
+
function move(from, to) {
|
|
479
|
+
try {
|
|
480
|
+
renameSync(from, to);
|
|
481
|
+
} catch (err) {
|
|
482
|
+
if (err.code !== 'EXDEV') throw err;
|
|
483
|
+
cpSync(from, to, { recursive: true });
|
|
484
|
+
if (lstatSync(to).isFile()) durable.syncFile(to);
|
|
485
|
+
rmSync(from, { recursive: true, force: true });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* The live database copied to `kept`, whole and on the disk. Written aside,
|
|
491
|
+
* synced (SQLite doesn't sync what VACUUM INTO writes), then renamed, and the
|
|
492
|
+
* rename synced: a kept copy that exists is a whole one, even after a power cut.
|
|
493
|
+
* Opening the database first replays a -wal left by a hard stop (nothing
|
|
494
|
+
* closes it on quit), so the copy holds everything.
|
|
495
|
+
*/
|
|
496
|
+
export function vacuumCopy(dbFile, kept) {
|
|
497
|
+
const partial = `${kept}.partial`;
|
|
498
|
+
rmSync(partial, { force: true });
|
|
499
|
+
try {
|
|
500
|
+
const current = new DatabaseSync(dbFile);
|
|
501
|
+
try { current.exec(`VACUUM INTO ${sqlString(partial)}`); } finally { current.close(); }
|
|
502
|
+
durable.syncFile(partial);
|
|
503
|
+
renameSync(partial, kept);
|
|
504
|
+
durable.syncFolder(path.dirname(kept));
|
|
505
|
+
} catch (err) {
|
|
506
|
+
rmSync(partial, { force: true });
|
|
507
|
+
throw err;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Is `kept` a whole copy of `original`? SQLite's quick check passes on it, and
|
|
513
|
+
* it has every table of the original with as many rows. An empty file (what a
|
|
514
|
+
* copy can be after a power cut) passes the quick check as an empty database,
|
|
515
|
+
* so the row counts are what give that away.
|
|
516
|
+
*/
|
|
517
|
+
export function keptCopyMatches(original, kept) {
|
|
518
|
+
let copy = null;
|
|
519
|
+
let live = null;
|
|
520
|
+
try {
|
|
521
|
+
copy = new DatabaseSync(kept, { readOnly: true });
|
|
522
|
+
const check = copy.prepare('PRAGMA quick_check').all();
|
|
523
|
+
if (check.length !== 1 || check[0].quick_check !== 'ok') return false;
|
|
524
|
+
// Not read-only: a read-only connection to a WAL database leaves its
|
|
525
|
+
// -wal and -shm behind, and this one is about to be replaced.
|
|
526
|
+
live = new DatabaseSync(original);
|
|
527
|
+
live.exec('PRAGMA query_only = ON');
|
|
528
|
+
const tables = (db) => db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'")
|
|
529
|
+
.all().map((r) => r.name);
|
|
530
|
+
const rows = (db, t) => db.prepare(`SELECT count(*) AS n FROM "${t.replace(/"/g, '""')}"`).get().n;
|
|
531
|
+
const inCopy = new Set(tables(copy));
|
|
532
|
+
return tables(live).every((t) => inCopy.has(t) && rows(live, t) === rows(copy, t));
|
|
533
|
+
} catch {
|
|
534
|
+
return false;
|
|
535
|
+
} finally {
|
|
536
|
+
copy?.close();
|
|
537
|
+
live?.close();
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Does SQLite's quick check pass on this database, and does it have the
|
|
543
|
+
* network's table? An empty file passes the check as an empty database.
|
|
544
|
+
*/
|
|
545
|
+
function holdsANetwork(file) {
|
|
546
|
+
let db = null;
|
|
547
|
+
try {
|
|
548
|
+
// Not read-only, for the same reason as above.
|
|
549
|
+
db = new DatabaseSync(file);
|
|
550
|
+
db.exec('PRAGMA query_only = ON');
|
|
551
|
+
const check = db.prepare('PRAGMA quick_check').all();
|
|
552
|
+
return check.length === 1 && check[0].quick_check === 'ok'
|
|
553
|
+
&& db.prepare("SELECT count(*) AS n FROM sqlite_master WHERE type = 'table' AND name = 'linkedin_connections'").get().n === 1;
|
|
554
|
+
} catch {
|
|
555
|
+
return false;
|
|
556
|
+
} finally {
|
|
557
|
+
db?.close();
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Does another process have this database open? Asked before anything moves:
|
|
563
|
+
* the swap renames the files under whoever holds them, and that process would
|
|
564
|
+
* go on writing into a copy nobody reads. The realistic case is a second copy
|
|
565
|
+
* of Six Degrees on the same folder, like `npm run dev` or `npx six-degrees`
|
|
566
|
+
* while the Mac app runs.
|
|
567
|
+
*
|
|
568
|
+
* Reading the database in exclusive locking mode needs every other connection
|
|
569
|
+
* gone: a process that has read it keeps a shared lock for as long as it has
|
|
570
|
+
* it open, idle or not, so the read is refused with "database is locked".
|
|
571
|
+
* Switching its journal mode away from WAL needs the same lock and is refused
|
|
572
|
+
* the same way, but it rewrites the file's header; this writes nothing, so a
|
|
573
|
+
* damaged database is still kept byte for byte, and there is nothing to put
|
|
574
|
+
* back afterwards.
|
|
575
|
+
*/
|
|
576
|
+
export function openElsewhere(dbFile) {
|
|
577
|
+
let db = null;
|
|
578
|
+
try {
|
|
579
|
+
db = new DatabaseSync(dbFile);
|
|
580
|
+
db.exec('PRAGMA locking_mode = EXCLUSIVE');
|
|
581
|
+
db.prepare('SELECT count(*) AS n FROM sqlite_master').get();
|
|
582
|
+
return false;
|
|
583
|
+
} catch (err) {
|
|
584
|
+
// SQLITE_BUSY or SQLITE_LOCKED: someone else has it. Anything else (a
|
|
585
|
+
// damaged file) is for the steps below to deal with.
|
|
586
|
+
return err.errcode === 5 || err.errcode === 6 || /database (table )?is locked/i.test(String(err.message));
|
|
587
|
+
} finally {
|
|
588
|
+
try { db?.close(); } catch { /* never opened */ }
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** The import waiting for the next start, or null. */
|
|
593
|
+
export function pendingImport(dir) {
|
|
594
|
+
const folder = path.join(dir, PENDING_DIR);
|
|
595
|
+
if (!exists(path.join(folder, 'READY'))) return null;
|
|
596
|
+
const ready = readJson(path.join(folder, 'READY')) || {};
|
|
597
|
+
const applying = readJson(path.join(folder, 'APPLYING'));
|
|
598
|
+
let error = null;
|
|
599
|
+
try { error = readFileSync(path.join(folder, 'ERROR'), 'utf8').trim() || null; } catch { /* none */ }
|
|
600
|
+
// Once anything has moved, the only way out is forward, at the next start.
|
|
601
|
+
// Deciding to keep the database as it is counts: it is recorded before the
|
|
602
|
+
// files move, and they may have.
|
|
603
|
+
const started = Boolean(applying && (applying.phase !== 'start' || applying.keptAsIs));
|
|
604
|
+
return { ...ready, started, error };
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** Throw away a staged import that hasn't started. Resolves { cancelled, reason? }. */
|
|
608
|
+
export function cancelPendingImport(dir) {
|
|
609
|
+
const current = pendingImport(dir);
|
|
610
|
+
if (!current) return { cancelled: false, reason: 'none' };
|
|
611
|
+
if (current.started) return { cancelled: false, reason: 'started' };
|
|
612
|
+
const folder = path.join(dir, PENDING_DIR);
|
|
613
|
+
const spent = `${folder}.cancelled-${Date.now()}`;
|
|
614
|
+
// One rename first, so a half-deleted folder is never read as an import.
|
|
615
|
+
renameSync(folder, spent);
|
|
616
|
+
rmSync(spent, { recursive: true, force: true });
|
|
617
|
+
return { cancelled: true };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Finish a staged import: called by getDb() before it opens the database.
|
|
622
|
+
*
|
|
623
|
+
* Each step is recorded in import-pending/APPLYING, on the disk, before the
|
|
624
|
+
* next begins, and whatever a step relies on is on the disk before it is
|
|
625
|
+
* recorded (lib/durable.js). So a start that dies part-way, even from a power
|
|
626
|
+
* cut, carries on from where it stopped next time rather than undoing or
|
|
627
|
+
* repeating anything.
|
|
628
|
+
*
|
|
629
|
+
* `copyDatabase` is how the network here is copied (tests pass a failing one).
|
|
630
|
+
*
|
|
631
|
+
* @returns what happened, for app_meta 'last_import', or null when nothing waits.
|
|
632
|
+
* @throws when a step fails; the reason is kept in import-pending/ERROR for the page.
|
|
633
|
+
*/
|
|
634
|
+
export function applyPendingImport({ dir, dbFile, now = new Date(), copyDatabase = vacuumCopy }) {
|
|
635
|
+
sweepLeftovers(dir, { now: now.getTime() });
|
|
636
|
+
const pending = path.join(dir, PENDING_DIR);
|
|
637
|
+
if (!exists(path.join(pending, 'READY'))) return null;
|
|
638
|
+
const ready = readJson(path.join(pending, 'READY')) || {};
|
|
639
|
+
|
|
640
|
+
const journal = path.join(pending, 'APPLYING');
|
|
641
|
+
const state = readJson(journal) || { stamp: now.toISOString().replace(/[:.]/g, '-'), phase: 'start' };
|
|
642
|
+
const record = () => durable.writeFileDurably(journal, JSON.stringify(state));
|
|
643
|
+
const step = (phase) => {
|
|
644
|
+
state.phase = phase;
|
|
645
|
+
record();
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
const dbFolder = path.dirname(dbFile);
|
|
649
|
+
const backups = path.join(dbFolder, 'backups');
|
|
650
|
+
const keptDb = path.join(backups, `${IMPORT_BACKUP_PREFIX}${state.stamp}.sqlite`);
|
|
651
|
+
const keptFiles = path.join(backups, `${IMPORT_BACKUP_PREFIX}${state.stamp}-files`);
|
|
652
|
+
const staged = path.join(pending, 'data.sqlite');
|
|
653
|
+
const SIDE_FILES = ['-wal', '-shm', '-journal'];
|
|
654
|
+
|
|
655
|
+
// The database kept as it is, renamed byte for byte with its -wal and -shm
|
|
656
|
+
// under the same name so they still pair up. Those go first: a -wal must
|
|
657
|
+
// never be left beside a database it doesn't belong to. A file already in
|
|
658
|
+
// its kept place stays there, so a start that stopped half-way through
|
|
659
|
+
// finishes without overwriting anything: what is left behind then (a -wal
|
|
660
|
+
// SQLite made since) goes with the old database at step 3.
|
|
661
|
+
const moveAsIs = () => {
|
|
662
|
+
for (const suffix of SIDE_FILES) {
|
|
663
|
+
if (exists(`${dbFile}${suffix}`) && !exists(`${keptDb}${suffix}`)) move(`${dbFile}${suffix}`, `${keptDb}${suffix}`);
|
|
664
|
+
}
|
|
665
|
+
if (exists(dbFile) && !exists(keptDb)) move(dbFile, keptDb);
|
|
666
|
+
durable.syncFolder(backups);
|
|
667
|
+
durable.syncFolder(dbFolder);
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
// 1. Keep the network that is here, in backups/.
|
|
671
|
+
let copiedNow = false;
|
|
672
|
+
const keep = () => {
|
|
673
|
+
mkdirSync(backups, { recursive: true });
|
|
674
|
+
try {
|
|
675
|
+
// Not SQLite at all: don't hand it to SQLite, which would open the
|
|
676
|
+
// -wal beside it, find nothing it can use, and delete it on close.
|
|
677
|
+
if (!looksLikeSqlite(dbFile)) throw new Error('it is not a SQLite database');
|
|
678
|
+
copyDatabase(dbFile, keptDb);
|
|
679
|
+
copiedNow = true;
|
|
680
|
+
} catch (err) {
|
|
681
|
+
// A database that can't be copied (damaged, or a disk too full for the
|
|
682
|
+
// copy) is kept as it is instead. Otherwise the import someone makes to
|
|
683
|
+
// recover from a damaged network could never finish. A rename needs no
|
|
684
|
+
// space, and step 3 would remove these files anyway, so nothing is lost.
|
|
685
|
+
// A copy an earlier try left under the kept name is stale: this file is
|
|
686
|
+
// the one to keep. The decision is on the disk before anything moves.
|
|
687
|
+
for (const suffix of ['', ...SIDE_FILES]) rmSync(`${keptDb}${suffix}`, { force: true });
|
|
688
|
+
rmSync(`${keptDb}.partial`, { force: true });
|
|
689
|
+
state.keptAsIs = String(err.message || err).slice(0, 200);
|
|
690
|
+
record();
|
|
691
|
+
moveAsIs();
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
if (!exists(journal)) record();
|
|
697
|
+
const beforeTheSwap = ['start', 'database-kept', 'files-kept'].includes(state.phase);
|
|
698
|
+
|
|
699
|
+
if (state.phase === 'start') {
|
|
700
|
+
// What is staged is checked once more before anything here changes: it
|
|
701
|
+
// could have been damaged since (a power cut as it was staged). Nothing
|
|
702
|
+
// has moved yet, so the page can still offer to cancel it.
|
|
703
|
+
if (!exists(staged) || !holdsANetwork(staged)) {
|
|
704
|
+
throw new Error('the imported network waiting here is damaged. Cancel the import and import the file again');
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Nothing moves while another copy of Six Degrees has this network open:
|
|
709
|
+
// its writes would land in a file nobody reads again.
|
|
710
|
+
if (beforeTheSwap && !state.keptAsIs && exists(dbFile) && looksLikeSqlite(dbFile) && openElsewhere(dbFile)) {
|
|
711
|
+
throw new Error('another copy of Six Degrees has this network open (one started with npx six-degrees or npm run dev, perhaps). Quit that copy, then restart this one');
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// A start that stopped while keeping the database as it was finishes that first.
|
|
715
|
+
if (beforeTheSwap && state.keptAsIs) moveAsIs();
|
|
716
|
+
|
|
717
|
+
if (state.phase === 'start') {
|
|
718
|
+
if (!state.keptAsIs && exists(dbFile)) keep();
|
|
719
|
+
step('database-kept');
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (state.phase === 'database-kept') {
|
|
723
|
+
// 2. The network's own files go beside it: its photos and the scanner's
|
|
724
|
+
// notes about its people. The LinkedIn budget stays where it is (it
|
|
725
|
+
// belongs to the account, not to the network: step 4 merges the
|
|
726
|
+
// copy's into it), and a copy of it goes beside them.
|
|
727
|
+
const keptFolder = () => mkdirSync(keptFiles, { recursive: true });
|
|
728
|
+
for (const name of ['avatars', ...NETWORK_FILES]) {
|
|
729
|
+
const from = path.join(dir, name);
|
|
730
|
+
if (!exists(from)) continue;
|
|
731
|
+
keptFolder();
|
|
732
|
+
move(from, path.join(keptFiles, name));
|
|
733
|
+
}
|
|
734
|
+
for (const name of BUDGET_FILES) {
|
|
735
|
+
const from = path.join(dir, name);
|
|
736
|
+
if (!exists(from) || !lstatSync(from).isFile()) continue;
|
|
737
|
+
keptFolder();
|
|
738
|
+
copyFileSync(from, path.join(keptFiles, name));
|
|
739
|
+
durable.syncFile(path.join(keptFiles, name));
|
|
740
|
+
}
|
|
741
|
+
if (exists(keptFiles)) durable.syncFolder(keptFiles);
|
|
742
|
+
durable.syncFolder(backups);
|
|
743
|
+
durable.syncFolder(dir);
|
|
744
|
+
step('files-kept');
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (state.phase === 'files-kept') {
|
|
748
|
+
// 3. The old database is replaced, but only once the copy kept of it is
|
|
749
|
+
// known to be whole. A copy an earlier start made is made again: it
|
|
750
|
+
// may never have reached the disk whole (a power cut), and the
|
|
751
|
+
// network here may have changed since (that start stopped, and the
|
|
752
|
+
// app ran on it). Once the new database is in place, this step is
|
|
753
|
+
// never checked again (the "original" would be the new network).
|
|
754
|
+
if (exists(staged)) {
|
|
755
|
+
if (!state.keptAsIs && exists(dbFile)) {
|
|
756
|
+
let whole = copiedNow && keptCopyMatches(dbFile, keptDb);
|
|
757
|
+
if (!whole) {
|
|
758
|
+
keep();
|
|
759
|
+
whole = Boolean(state.keptAsIs) || keptCopyMatches(dbFile, keptDb);
|
|
760
|
+
}
|
|
761
|
+
if (!whole) throw new Error("the copy of the network here couldn't be checked, so it wasn't replaced");
|
|
762
|
+
}
|
|
763
|
+
// The old database goes together with its -wal and -shm: a stale WAL
|
|
764
|
+
// beside the new file would be replayed into it and corrupt it. The
|
|
765
|
+
// new one takes its place in a single rename.
|
|
766
|
+
for (const suffix of SIDE_FILES) rmSync(`${dbFile}${suffix}`, { force: true });
|
|
767
|
+
mkdirSync(dbFolder, { recursive: true });
|
|
768
|
+
move(staged, dbFile);
|
|
769
|
+
durable.syncFolder(dbFolder);
|
|
770
|
+
}
|
|
771
|
+
step('database-placed');
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (state.phase === 'database-placed') {
|
|
775
|
+
// 4. The photos and files that came with it. The network's own files
|
|
776
|
+
// replace this computer's (which are kept since step 2); the LinkedIn
|
|
777
|
+
// budget is merged (lib/linkedin-limits.js mergeBudgetFiles).
|
|
778
|
+
const files = path.join(pending, 'files');
|
|
779
|
+
const photos = path.join(files, 'avatars');
|
|
780
|
+
if (exists(photos)) {
|
|
781
|
+
mkdirSync(path.join(dir, 'avatars'), { recursive: true });
|
|
782
|
+
for (const name of namesInFolder(photos)) move(path.join(photos, name), path.join(dir, 'avatars', name));
|
|
783
|
+
durable.syncFolder(path.join(dir, 'avatars'));
|
|
784
|
+
}
|
|
785
|
+
for (const name of NETWORK_FILES) {
|
|
786
|
+
const from = path.join(files, name);
|
|
787
|
+
if (exists(from)) move(from, path.join(dir, name));
|
|
788
|
+
}
|
|
789
|
+
mergeBudgetFiles({ dir, from: files });
|
|
790
|
+
durable.syncFolder(dir);
|
|
791
|
+
step('done');
|
|
792
|
+
}
|
|
793
|
+
} catch (err) {
|
|
794
|
+
try { writeFileSync(path.join(pending, 'ERROR'), `${err.message}\n`); } catch { /* the folder went too */ }
|
|
795
|
+
throw err;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// 5. Out of the way in one rename, then deleted.
|
|
799
|
+
const spent = `${pending}.done-${state.stamp}`;
|
|
800
|
+
renameSync(pending, spent);
|
|
801
|
+
rmSync(spent, { recursive: true, force: true });
|
|
802
|
+
|
|
803
|
+
const shown = (p) => {
|
|
804
|
+
const rel = path.relative(dir, p);
|
|
805
|
+
return rel && !rel.startsWith('..') && !path.isAbsolute(rel) ? rel : p;
|
|
806
|
+
};
|
|
807
|
+
return {
|
|
808
|
+
appliedAt: now.toISOString(),
|
|
809
|
+
stagedAt: ready.stagedAt ?? null,
|
|
810
|
+
exportedAt: ready.exportedAt ?? null,
|
|
811
|
+
fromVersion: ready.fromVersion ?? null,
|
|
812
|
+
people: ready.people ?? null,
|
|
813
|
+
photos: ready.photos ?? null,
|
|
814
|
+
replacedPeople: ready.replacedPeople ?? null,
|
|
815
|
+
keptDatabase: exists(keptDb) ? shown(keptDb) : null,
|
|
816
|
+
keptFiles: exists(keptFiles) ? shown(keptFiles) : null,
|
|
817
|
+
// Set when the database that was here couldn't be read, so it was kept
|
|
818
|
+
// as it was rather than copied: the reason SQLite gave.
|
|
819
|
+
keptAsIs: state.keptAsIs ?? null,
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/** Remember the finished import in the new database, for the Settings page. */
|
|
824
|
+
export function recordImport(db, result) {
|
|
825
|
+
db.prepare(`INSERT INTO app_meta (key, value) VALUES ('last_import', ?)
|
|
826
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(JSON.stringify(result));
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
export function lastImport(db) {
|
|
830
|
+
try {
|
|
831
|
+
const value = db.prepare("SELECT value FROM app_meta WHERE key = 'last_import'").get()?.value;
|
|
832
|
+
const parsed = value ? JSON.parse(value) : null;
|
|
833
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
834
|
+
} catch {
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// ── restarting to finish ─────────────────────────────────────────────────────
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* The exit code this copy's launcher treats as "start the server again", or
|
|
843
|
+
* null when it can't. Only the Mac app's shell sets it (desktop/main.mjs); an
|
|
844
|
+
* npx or source copy runs the server in the Terminal, which a restart would end.
|
|
845
|
+
*/
|
|
846
|
+
export function restartCodeFrom(env = process.env) {
|
|
847
|
+
const n = Number(env.SIX_DEGREES_RESTART_CODE);
|
|
848
|
+
return Number.isInteger(n) && n > 1 && n < 126 ? n : null;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* How to restart this copy to finish an import, in words for the page. Shown
|
|
853
|
+
* before anyone has clicked anything, so it says what to do, not what is
|
|
854
|
+
* happening.
|
|
855
|
+
*/
|
|
856
|
+
export function restartAdvice({ kind, code, dataDir = null }) {
|
|
857
|
+
const finish = 'It finishes the import as it starts.';
|
|
858
|
+
if (code) {
|
|
859
|
+
return { canRestart: true, how: 'Click Restart now to finish it: Six Degrees restarts, which takes a few seconds, and finishes the import as it starts.' };
|
|
860
|
+
}
|
|
861
|
+
if (kind === 'mac-app') return { canRestart: false, how: `Quit Six Degrees (⌘Q) and open it again. ${finish}` };
|
|
862
|
+
if (kind === 'npm') {
|
|
863
|
+
const flag = dataDir ? ` --data-dir '${String(dataDir).replace(/'/g, `'\\''`)}'` : '';
|
|
864
|
+
return {
|
|
865
|
+
canRestart: false,
|
|
866
|
+
how: `Stop Six Degrees with Ctrl-C in its Terminal window, then start it again. ${finish}`,
|
|
867
|
+
command: `npx six-degrees${flag}`,
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
if (kind === 'source') {
|
|
871
|
+
return {
|
|
872
|
+
canRestart: false,
|
|
873
|
+
how: `Stop Six Degrees with Ctrl-C in its Terminal window, then start it again. ${finish}`,
|
|
874
|
+
command: 'npm run start:packaged',
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
return { canRestart: false, how: `Stop the server with Ctrl-C and start it again the way you started it. ${finish}` };
|
|
878
|
+
}
|