dineway 0.1.3 → 0.1.5
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/README.md +6 -3
- package/dist/{apply-CAPvMfoU.mjs → apply-iVSqz2qs.mjs} +132 -39
- package/dist/astro/index.d.mts +18 -9
- package/dist/astro/index.mjs +238 -16
- package/dist/astro/middleware/auth.d.mts +16 -5
- package/dist/astro/middleware/auth.mjs +74 -37
- package/dist/astro/middleware/redirect.mjs +24 -8
- package/dist/astro/middleware/request-context.mjs +18 -5
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.mjs +411 -169
- package/dist/astro/types.d.mts +25 -8
- package/dist/{byline-DeWCMU_i.mjs → byline-OhH2dlRu.mjs} +6 -21
- package/dist/{bylines-DyqBV9EQ.mjs → bylines-BGpD9_hy.mjs} +16 -6
- package/dist/cache-BdSY-gQN.mjs +42 -0
- package/dist/chunks--4F8ddV4.mjs +18 -0
- package/dist/cli/index.mjs +935 -15
- package/dist/client/external-auth-headers.d.mts +1 -1
- package/dist/client/index.d.mts +11 -3
- package/dist/client/index.mjs +4 -3
- package/dist/{connection-C9pxzuag.mjs → connection-BCNICDWN.mjs} +22 -5
- package/dist/{content-zSgdNmnt.mjs → content-DWi4d0rT.mjs} +41 -2
- package/dist/database/instrumentation.d.mts +34 -0
- package/dist/database/instrumentation.mjs +53 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +2 -2
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/libsql.mjs +11 -5
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db/sqlite.mjs +7 -1
- package/dist/db-errors-CEqD7qH9.mjs +23 -0
- package/dist/{default-WYlzADZL.mjs → default-VjJyuuG9.mjs} +2 -0
- package/dist/{dialect-helpers-B9uSp2GJ.mjs → dialect-helpers-DhTzaUxP.mjs} +3 -0
- package/dist/{error-DrxtnGPg.mjs → error-BmL6QipT.mjs} +7 -3
- package/dist/{index-C-jx21qs.d.mts → index-yvc6E_17.d.mts} +157 -30
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +24 -22
- package/dist/{loader-qKmo0wAY.mjs → loader-sMG4TZ-u.mjs} +9 -3
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/page/index.d.mts +10 -2
- package/dist/page/index.mjs +22 -1
- package/dist/patterns-CrCYkMBb.mjs +92 -0
- package/dist/{placeholder-bOx1xCTY.d.mts → placeholder--wOi4TbO.d.mts} +1 -1
- package/dist/{placeholder-B3knXwNc.mjs → placeholder-Cp8g5Emj.mjs} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-BiaPl_g2.mjs → query-kDmwCsHh.mjs} +118 -50
- package/dist/{redirect-JPqLAbxa.mjs → redirect-DnEWAkVg.mjs} +43 -99
- package/dist/{registry-DSd1GWB8.mjs → registry-C0zjeB9P.mjs} +191 -123
- package/dist/request-cache-Dk5qPSOx.mjs +66 -0
- package/dist/request-context.d.mts +4 -16
- package/dist/{runner-B5l1JfOj.d.mts → runner-CFI6B6J2.d.mts} +1 -1
- package/dist/{runner-BGUGywgG.mjs → runner-DWZm2KQm.mjs} +589 -137
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +2 -2
- package/dist/{search-BNruJHDL.mjs → search-ByRGV2pq.mjs} +570 -424
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +11 -10
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +11 -3
- package/dist/storage/s3.mjs +78 -15
- package/dist/taxonomies-1s5PaS_8.mjs +266 -0
- package/dist/transaction-Cn2rjY78.mjs +27 -0
- package/dist/{types-BgQeVaPj.d.mts → types-BuMDPy5C.d.mts} +52 -3
- package/dist/{types-DuNbGKjF.mjs → types-COeOq9nK.mjs} +6 -1
- package/dist/{types-ju-_ORz7.d.mts → types-CWbdtiux.d.mts} +13 -5
- package/dist/{types-D38djUXv.d.mts → types-Cj0KMIZV.d.mts} +16 -3
- package/dist/{types-DkvMXalq.d.mts → types-DOrVigru.d.mts} +159 -0
- package/dist/{validate-CXnRKfJK.mjs → validate-BZ5wnLLp.mjs} +2 -1
- package/dist/{validate-DVKJJ-M_.d.mts → validate-IPf8n4Fj.d.mts} +4 -51
- package/dist/{validate-CqRJb_xU.mjs → validate-VPnKoIzW.mjs} +10 -10
- package/dist/version-BKXPsfmJ.mjs +6 -0
- package/package.json +53 -39
- package/src/astro/routes/PluginRegistry.tsx +21 -0
- package/src/astro/routes/admin.astro +99 -0
- package/src/astro/routes/api/admin/allowed-domains/[domain].ts +112 -0
- package/src/astro/routes/api/admin/allowed-domains/index.ts +108 -0
- package/src/astro/routes/api/admin/api-tokens/[id].ts +44 -0
- package/src/astro/routes/api/admin/api-tokens/index.ts +90 -0
- package/src/astro/routes/api/admin/briefing.ts +76 -0
- package/src/astro/routes/api/admin/bylines/[id]/index.ts +90 -0
- package/src/astro/routes/api/admin/bylines/index.ts +74 -0
- package/src/astro/routes/api/admin/comments/[id]/status.ts +120 -0
- package/src/astro/routes/api/admin/comments/[id].ts +64 -0
- package/src/astro/routes/api/admin/comments/bulk.ts +42 -0
- package/src/astro/routes/api/admin/comments/counts.ts +30 -0
- package/src/astro/routes/api/admin/comments/index.ts +46 -0
- package/src/astro/routes/api/admin/context/[id]/history.ts +35 -0
- package/src/astro/routes/api/admin/context/[id]/index.ts +35 -0
- package/src/astro/routes/api/admin/context/[id]/review.ts +57 -0
- package/src/astro/routes/api/admin/context/[id]/supersede.ts +58 -0
- package/src/astro/routes/api/admin/context/diff.ts +35 -0
- package/src/astro/routes/api/admin/context/index.ts +69 -0
- package/src/astro/routes/api/admin/context/stale.ts +35 -0
- package/src/astro/routes/api/admin/hitl-requests/[id]/index.ts +38 -0
- package/src/astro/routes/api/admin/hitl-requests/[id]/resolve.ts +54 -0
- package/src/astro/routes/api/admin/hitl-requests/index.ts +38 -0
- package/src/astro/routes/api/admin/hooks/exclusive/[hookName].ts +132 -0
- package/src/astro/routes/api/admin/hooks/exclusive/index.ts +51 -0
- package/src/astro/routes/api/admin/oauth-clients/[id].ts +137 -0
- package/src/astro/routes/api/admin/oauth-clients/index.ts +95 -0
- package/src/astro/routes/api/admin/plugins/[id]/disable.ts +91 -0
- package/src/astro/routes/api/admin/plugins/[id]/enable.ts +91 -0
- package/src/astro/routes/api/admin/plugins/[id]/index.ts +38 -0
- package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +98 -0
- package/src/astro/routes/api/admin/plugins/[id]/update.ts +154 -0
- package/src/astro/routes/api/admin/plugins/index.ts +32 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/icon.ts +62 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/index.ts +33 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +135 -0
- package/src/astro/routes/api/admin/plugins/marketplace/index.ts +38 -0
- package/src/astro/routes/api/admin/plugins/updates.ts +28 -0
- package/src/astro/routes/api/admin/review-requests/[id]/index.ts +35 -0
- package/src/astro/routes/api/admin/review-requests/[id]/resolve.ts +52 -0
- package/src/astro/routes/api/admin/review-requests/index.ts +35 -0
- package/src/astro/routes/api/admin/themes/marketplace/[id]/index.ts +33 -0
- package/src/astro/routes/api/admin/themes/marketplace/[id]/thumbnail.ts +62 -0
- package/src/astro/routes/api/admin/themes/marketplace/index.ts +45 -0
- package/src/astro/routes/api/admin/users/[id]/disable.ts +72 -0
- package/src/astro/routes/api/admin/users/[id]/enable.ts +48 -0
- package/src/astro/routes/api/admin/users/[id]/index.ts +166 -0
- package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +72 -0
- package/src/astro/routes/api/admin/users/index.ts +66 -0
- package/src/astro/routes/api/auth/dev-bypass.ts +139 -0
- package/src/astro/routes/api/auth/invite/accept.ts +52 -0
- package/src/astro/routes/api/auth/invite/complete.ts +86 -0
- package/src/astro/routes/api/auth/invite/index.ts +99 -0
- package/src/astro/routes/api/auth/invite/register-options.ts +73 -0
- package/src/astro/routes/api/auth/logout.ts +40 -0
- package/src/astro/routes/api/auth/magic-link/send.ts +90 -0
- package/src/astro/routes/api/auth/magic-link/verify.ts +71 -0
- package/src/astro/routes/api/auth/me.ts +60 -0
- package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +221 -0
- package/src/astro/routes/api/auth/oauth/[provider].ts +120 -0
- package/src/astro/routes/api/auth/passkey/[id].ts +124 -0
- package/src/astro/routes/api/auth/passkey/index.ts +54 -0
- package/src/astro/routes/api/auth/passkey/options.ts +85 -0
- package/src/astro/routes/api/auth/passkey/register/options.ts +88 -0
- package/src/astro/routes/api/auth/passkey/register/verify.ts +119 -0
- package/src/astro/routes/api/auth/passkey/verify.ts +72 -0
- package/src/astro/routes/api/auth/signup/complete.ts +87 -0
- package/src/astro/routes/api/auth/signup/request.ts +89 -0
- package/src/astro/routes/api/auth/signup/verify.ts +53 -0
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +310 -0
- package/src/astro/routes/api/content/[collection]/[id]/compare.ts +28 -0
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +68 -0
- package/src/astro/routes/api/content/[collection]/[id]/duplicate.ts +77 -0
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +42 -0
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +107 -0
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +100 -0
- package/src/astro/routes/api/content/[collection]/[id]/restore.ts +64 -0
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +31 -0
- package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +129 -0
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +143 -0
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +50 -0
- package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +69 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +173 -0
- package/src/astro/routes/api/content/[collection]/index.ts +103 -0
- package/src/astro/routes/api/content/[collection]/trash.ts +33 -0
- package/src/astro/routes/api/dashboard.ts +32 -0
- package/src/astro/routes/api/dev/emails.ts +36 -0
- package/src/astro/routes/api/health.ts +54 -0
- package/src/astro/routes/api/import/probe.ts +47 -0
- package/src/astro/routes/api/import/wordpress/analyze.ts +523 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +330 -0
- package/src/astro/routes/api/import/wordpress/media.ts +338 -0
- package/src/astro/routes/api/import/wordpress/prepare.ts +212 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +425 -0
- package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +111 -0
- package/src/astro/routes/api/import/wordpress-plugin/callback.ts +58 -0
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +399 -0
- package/src/astro/routes/api/manifest.ts +75 -0
- package/src/astro/routes/api/mcp.ts +125 -0
- package/src/astro/routes/api/media/[id]/confirm.ts +93 -0
- package/src/astro/routes/api/media/[id].ts +145 -0
- package/src/astro/routes/api/media/file/[...key].ts +79 -0
- package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +91 -0
- package/src/astro/routes/api/media/providers/[providerId]/index.ts +111 -0
- package/src/astro/routes/api/media/providers/index.ts +30 -0
- package/src/astro/routes/api/media/upload-url.ts +146 -0
- package/src/astro/routes/api/media.ts +204 -0
- package/src/astro/routes/api/menus/[name]/items.ts +206 -0
- package/src/astro/routes/api/menus/[name]/reorder.ts +79 -0
- package/src/astro/routes/api/menus/[name].ts +145 -0
- package/src/astro/routes/api/menus/index.ts +91 -0
- package/src/astro/routes/api/oauth/authorize.ts +430 -0
- package/src/astro/routes/api/oauth/device/authorize.ts +45 -0
- package/src/astro/routes/api/oauth/device/code.ts +56 -0
- package/src/astro/routes/api/oauth/device/token.ts +70 -0
- package/src/astro/routes/api/oauth/register.ts +182 -0
- package/src/astro/routes/api/oauth/token/refresh.ts +38 -0
- package/src/astro/routes/api/oauth/token/revoke.ts +38 -0
- package/src/astro/routes/api/oauth/token.ts +195 -0
- package/src/astro/routes/api/openapi.json.ts +33 -0
- package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +109 -0
- package/src/astro/routes/api/redirects/404s/index.ts +72 -0
- package/src/astro/routes/api/redirects/404s/summary.ts +33 -0
- package/src/astro/routes/api/redirects/[id].ts +183 -0
- package/src/astro/routes/api/redirects/index.ts +100 -0
- package/src/astro/routes/api/revisions/[revisionId]/index.ts +29 -0
- package/src/astro/routes/api/revisions/[revisionId]/restore.ts +62 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +104 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +67 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +45 -0
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +107 -0
- package/src/astro/routes/api/schema/collections/index.ts +61 -0
- package/src/astro/routes/api/schema/index.ts +109 -0
- package/src/astro/routes/api/schema/orphans/[slug].ts +36 -0
- package/src/astro/routes/api/schema/orphans/index.ts +26 -0
- package/src/astro/routes/api/search/enable.ts +64 -0
- package/src/astro/routes/api/search/index.ts +52 -0
- package/src/astro/routes/api/search/rebuild.ts +72 -0
- package/src/astro/routes/api/search/stats.ts +35 -0
- package/src/astro/routes/api/search/suggest.ts +50 -0
- package/src/astro/routes/api/sections/[slug].ts +203 -0
- package/src/astro/routes/api/sections/index.ts +107 -0
- package/src/astro/routes/api/settings/email.ts +150 -0
- package/src/astro/routes/api/settings.ts +116 -0
- package/src/astro/routes/api/setup/admin-verify.ts +122 -0
- package/src/astro/routes/api/setup/admin.ts +104 -0
- package/src/astro/routes/api/setup/dev-bypass.ts +200 -0
- package/src/astro/routes/api/setup/dev-reset.ts +40 -0
- package/src/astro/routes/api/setup/index.ts +128 -0
- package/src/astro/routes/api/setup/status.ts +122 -0
- package/src/astro/routes/api/snapshot.ts +76 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +232 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +131 -0
- package/src/astro/routes/api/taxonomies/index.ts +114 -0
- package/src/astro/routes/api/themes/preview.ts +78 -0
- package/src/astro/routes/api/typegen.ts +114 -0
- package/src/astro/routes/api/well-known/auth.ts +71 -0
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +48 -0
- package/src/astro/routes/api/well-known/oauth-protected-resource.ts +39 -0
- package/src/astro/routes/api/widget-areas/[name]/reorder.ts +114 -0
- package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +213 -0
- package/src/astro/routes/api/widget-areas/[name]/widgets.ts +126 -0
- package/src/astro/routes/api/widget-areas/[name].ts +135 -0
- package/src/astro/routes/api/widget-areas/index.ts +149 -0
- package/src/astro/routes/api/widget-components.ts +22 -0
- package/src/astro/routes/robots.txt.ts +81 -0
- package/src/astro/routes/sitemap-[collection].xml.ts +104 -0
- package/src/astro/routes/sitemap.xml.ts +92 -0
- package/src/components/Break.astro +45 -0
- package/src/components/Button.astro +71 -0
- package/src/components/Buttons.astro +49 -0
- package/src/components/Code.astro +59 -0
- package/src/components/Columns.astro +59 -0
- package/src/components/CommentForm.astro +315 -0
- package/src/components/Comments.astro +232 -0
- package/src/components/Cover.astro +128 -0
- package/src/components/DinewayBodyEnd.astro +32 -0
- package/src/components/DinewayBodyStart.astro +32 -0
- package/src/components/DinewayHead.astro +61 -0
- package/src/components/DinewayImage.astro +178 -0
- package/src/components/DinewayMedia.astro +167 -0
- package/src/components/Embed.astro +128 -0
- package/src/components/File.astro +122 -0
- package/src/components/Gallery.astro +93 -0
- package/src/components/HtmlBlock.astro +33 -0
- package/src/components/Image.astro +178 -0
- package/src/components/InlineEditor.astro +27 -0
- package/src/components/InlinePortableTextEditor.tsx +1937 -0
- package/src/components/LiveSearch.astro +614 -0
- package/src/components/PortableText.astro +51 -0
- package/src/components/Pullquote.astro +51 -0
- package/src/components/Table.astro +135 -0
- package/src/components/WidgetArea.astro +22 -0
- package/src/components/WidgetRenderer.astro +72 -0
- package/src/components/index.ts +106 -0
- package/src/components/marks/Link.astro +31 -0
- package/src/components/marks/StrikeThrough.astro +7 -0
- package/src/components/marks/Subscript.astro +7 -0
- package/src/components/marks/Superscript.astro +7 -0
- package/src/components/marks/Underline.astro +7 -0
- package/src/components/marks.ts +19 -0
- package/src/components/widgets/Archives.astro +65 -0
- package/src/components/widgets/Categories.astro +35 -0
- package/src/components/widgets/RecentPosts.astro +51 -0
- package/src/components/widgets/Search.astro +18 -0
- package/src/components/widgets/Tags.astro +38 -0
- package/src/ui.ts +75 -0
- package/LICENSE +0 -9
- /package/dist/{adapters-BlzWJG82.d.mts → adapters-C2ypTrZZ.d.mts} +0 -0
- /package/dist/{config-Cq8H0SfX.mjs → config-BXwuX8Bx.mjs} +0 -0
- /package/dist/{load-C6FCD1FU.mjs → load-Coc9HpHH.mjs} +0 -0
- /package/dist/{manifest-schema-CTSEyIJ3.mjs → manifest-schema-D1MSVnoI.mjs} +0 -0
- /package/dist/{mode-BlyYtIFO.mjs → mode-47goXBBK.mjs} +0 -0
- /package/dist/{tokens-4vgYuXsZ.mjs → tokens-CJz9ubV6.mjs} +0 -0
- /package/dist/{transport-C5FYnid7.mjs → transport-DB5eDN4x.mjs} +0 -0
- /package/dist/{transport-gIL-e43D.d.mts → transport-Wge_IzKl.d.mts} +0 -0
- /package/dist/{types-CLLdsG3g.d.mts → types-BzcUjoqg.d.mts} +0 -0
- /package/dist/{types-DShnjzb6.mjs → types-griIBQOQ.mjs} +0 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress WXR execute import endpoint
|
|
3
|
+
*
|
|
4
|
+
* POST /_dineway/api/import/wordpress/execute
|
|
5
|
+
*
|
|
6
|
+
* Accepts WXR file and import configuration, imports content into the database.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { gutenbergToPortableText } from "@dineway-ai/gutenberg-to-portable-text";
|
|
10
|
+
import type { APIRoute } from "astro";
|
|
11
|
+
import {
|
|
12
|
+
parseWxrString,
|
|
13
|
+
ContentRepository,
|
|
14
|
+
importReusableBlocksAsSections,
|
|
15
|
+
type WxrPost,
|
|
16
|
+
parseWxrDate,
|
|
17
|
+
} from "dineway";
|
|
18
|
+
|
|
19
|
+
import { requirePerm } from "#api/authorize.js";
|
|
20
|
+
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
21
|
+
import {
|
|
22
|
+
ensureWorkflowHitlRouteRequest,
|
|
23
|
+
hitlRequiredRouteError,
|
|
24
|
+
resolveHitlRouteActor,
|
|
25
|
+
} from "#api/hitl-route-helpers.js";
|
|
26
|
+
import { BylineRepository } from "#db/repositories/byline.js";
|
|
27
|
+
import { resolveImportByline } from "#import/utils.js";
|
|
28
|
+
import { sanitizeWordPressImportSlug } from "#import/wordpress-slugs.js";
|
|
29
|
+
import { RiskPolicyEvaluator, WordPressImportHitlPayloadBuilder } from "#site-context/index.js";
|
|
30
|
+
import type { DinewayHandlers, DinewayManifest } from "#types";
|
|
31
|
+
import { slugify } from "#utils/slugify.js";
|
|
32
|
+
|
|
33
|
+
export const prerender = false;
|
|
34
|
+
|
|
35
|
+
export interface ImportConfig {
|
|
36
|
+
/** Map WordPress post types to Dineway collections */
|
|
37
|
+
postTypeMappings: Record<
|
|
38
|
+
string,
|
|
39
|
+
{
|
|
40
|
+
collection: string;
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
}
|
|
43
|
+
>;
|
|
44
|
+
/** Whether to skip items that already exist (by slug) */
|
|
45
|
+
skipExisting: boolean;
|
|
46
|
+
/** Whether to import reusable blocks (wp_block) as sections */
|
|
47
|
+
importSections?: boolean;
|
|
48
|
+
/** Author mappings (WP author login -> Dineway user ID) */
|
|
49
|
+
authorMappings?: Record<string, string | null>;
|
|
50
|
+
/** BCP 47 locale for all imported items. When omitted, defaults to defaultLocale. */
|
|
51
|
+
locale?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ImportResult {
|
|
55
|
+
success: boolean;
|
|
56
|
+
imported: number;
|
|
57
|
+
skipped: number;
|
|
58
|
+
errors: Array<{ title: string; error: string }>;
|
|
59
|
+
byCollection: Record<string, number>;
|
|
60
|
+
/** Sections import results (if enabled) */
|
|
61
|
+
sections?: {
|
|
62
|
+
created: number;
|
|
63
|
+
skipped: number;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const POST: APIRoute = async ({ request, locals }) => {
|
|
68
|
+
const { dineway, dinewayManifest, user } = locals;
|
|
69
|
+
|
|
70
|
+
const denied = requirePerm(user, "import:execute");
|
|
71
|
+
if (denied) return denied;
|
|
72
|
+
|
|
73
|
+
if (!dineway?.handleContentCreate) {
|
|
74
|
+
return apiError("NOT_CONFIGURED", "Dineway is not configured", 500);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const formData = await request.formData();
|
|
79
|
+
const fileEntry = formData.get("file");
|
|
80
|
+
const file = fileEntry instanceof File ? fileEntry : null;
|
|
81
|
+
const configEntry = formData.get("config");
|
|
82
|
+
const configJson = typeof configEntry === "string" ? configEntry : null;
|
|
83
|
+
const hitlRequestEntry = formData.get("hitlRequestId");
|
|
84
|
+
const hitlRequestId =
|
|
85
|
+
typeof hitlRequestEntry === "string" && hitlRequestEntry.length > 0
|
|
86
|
+
? hitlRequestEntry
|
|
87
|
+
: undefined;
|
|
88
|
+
|
|
89
|
+
if (!file) {
|
|
90
|
+
return apiError("VALIDATION_ERROR", "No file provided", 400);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!configJson) {
|
|
94
|
+
return apiError("VALIDATION_ERROR", "No config provided", 400);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const config: ImportConfig = JSON.parse(configJson);
|
|
98
|
+
const actor = resolveHitlRouteActor(locals);
|
|
99
|
+
const evaluator = new RiskPolicyEvaluator({
|
|
100
|
+
db: dineway.db,
|
|
101
|
+
handlers: dineway,
|
|
102
|
+
});
|
|
103
|
+
let text: string;
|
|
104
|
+
if (evaluator.requiresWorkflowHitl(actor.identity)) {
|
|
105
|
+
const fileBuffer = await file.arrayBuffer();
|
|
106
|
+
const action = await new WordPressImportHitlPayloadBuilder().buildExecuteRequest({
|
|
107
|
+
fileName: file.name,
|
|
108
|
+
fileBuffer,
|
|
109
|
+
config,
|
|
110
|
+
});
|
|
111
|
+
const decision = await evaluator.evaluateWorkflowHitl({
|
|
112
|
+
actor: actor.identity,
|
|
113
|
+
hitlRequestId,
|
|
114
|
+
action,
|
|
115
|
+
});
|
|
116
|
+
if (!decision.allowed) {
|
|
117
|
+
const ensured = await ensureWorkflowHitlRouteRequest(dineway.db, locals, decision.action);
|
|
118
|
+
return hitlRequiredRouteError(decision, ensured);
|
|
119
|
+
}
|
|
120
|
+
text = new TextDecoder().decode(fileBuffer);
|
|
121
|
+
} else {
|
|
122
|
+
text = await file.text();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const wxr = await parseWxrString(text);
|
|
126
|
+
|
|
127
|
+
// Build attachment ID -> URL map for featured images
|
|
128
|
+
const attachmentMap = new Map<string, string>();
|
|
129
|
+
for (const att of wxr.attachments) {
|
|
130
|
+
if (att.id && att.url) {
|
|
131
|
+
attachmentMap.set(String(att.id), att.url);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Build author login -> display name map
|
|
136
|
+
const authorDisplayNames = new Map<string, string>();
|
|
137
|
+
for (const author of wxr.authors) {
|
|
138
|
+
if (!author.login) continue;
|
|
139
|
+
authorDisplayNames.set(author.login, author.displayName || author.login);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Import content (locale from config scopes all items)
|
|
143
|
+
const result = await importContent(
|
|
144
|
+
wxr.posts,
|
|
145
|
+
config,
|
|
146
|
+
dineway,
|
|
147
|
+
dinewayManifest,
|
|
148
|
+
attachmentMap,
|
|
149
|
+
config.locale,
|
|
150
|
+
authorDisplayNames,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// Import reusable blocks as sections (if enabled)
|
|
154
|
+
if (config.importSections !== false) {
|
|
155
|
+
const sectionsResult = await importReusableBlocksAsSections(wxr.posts, dineway.db);
|
|
156
|
+
result.sections = {
|
|
157
|
+
created: sectionsResult.sectionsCreated,
|
|
158
|
+
skipped: sectionsResult.sectionsSkipped,
|
|
159
|
+
};
|
|
160
|
+
// Add section errors to main errors array
|
|
161
|
+
result.errors.push(...sectionsResult.errors);
|
|
162
|
+
if (sectionsResult.errors.length > 0) {
|
|
163
|
+
result.success = false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return apiSuccess(result);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
return handleError(error, "Failed to import content", "WXR_IMPORT_ERROR");
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
async function importContent(
|
|
174
|
+
posts: WxrPost[],
|
|
175
|
+
config: ImportConfig,
|
|
176
|
+
dineway: DinewayHandlers,
|
|
177
|
+
manifest: DinewayManifest,
|
|
178
|
+
attachmentMap: Map<string, string>,
|
|
179
|
+
locale?: string,
|
|
180
|
+
authorDisplayNames?: Map<string, string>,
|
|
181
|
+
): Promise<ImportResult> {
|
|
182
|
+
const result: ImportResult = {
|
|
183
|
+
success: true,
|
|
184
|
+
imported: 0,
|
|
185
|
+
skipped: 0,
|
|
186
|
+
errors: [],
|
|
187
|
+
byCollection: {},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Create content repository for checking existing items
|
|
191
|
+
const contentRepo = new ContentRepository(dineway.db);
|
|
192
|
+
const bylineRepo = new BylineRepository(dineway.db);
|
|
193
|
+
const bylineCache = new Map<string, string>();
|
|
194
|
+
|
|
195
|
+
for (const post of posts) {
|
|
196
|
+
const postType = post.postType || "post";
|
|
197
|
+
const mapping = config.postTypeMappings[postType];
|
|
198
|
+
|
|
199
|
+
// Skip if not mapped or disabled
|
|
200
|
+
if (!mapping || !mapping.enabled) {
|
|
201
|
+
result.skipped++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Defensive: mapping.collection is already sanitized by prepare, but the user
|
|
206
|
+
// could manually edit the import config between prepare and execute.
|
|
207
|
+
const collection = sanitizeWordPressImportSlug(mapping.collection);
|
|
208
|
+
|
|
209
|
+
// Check if collection exists in manifest
|
|
210
|
+
if (!manifest?.collections[collection]) {
|
|
211
|
+
result.errors.push({
|
|
212
|
+
title: post.title || "Untitled",
|
|
213
|
+
error: `Collection "${collection}" does not exist`,
|
|
214
|
+
});
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
// Convert content to Portable Text
|
|
220
|
+
const content = post.content ? gutenbergToPortableText(post.content) : [];
|
|
221
|
+
|
|
222
|
+
// Generate slug from post name or title
|
|
223
|
+
const slug = post.postName || slugify(post.title || `post-${post.id || Date.now()}`);
|
|
224
|
+
|
|
225
|
+
// Check if already exists (idempotency)
|
|
226
|
+
if (config.skipExisting) {
|
|
227
|
+
const existing = await contentRepo.findBySlug(collection, slug);
|
|
228
|
+
if (existing) {
|
|
229
|
+
result.skipped++;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Map WordPress status to Dineway status
|
|
235
|
+
const status = mapStatus(post.status);
|
|
236
|
+
|
|
237
|
+
// Build data object with required fields
|
|
238
|
+
const data: Record<string, unknown> = {
|
|
239
|
+
title: post.title || "Untitled",
|
|
240
|
+
content,
|
|
241
|
+
excerpt: post.excerpt || undefined,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// Only add featured_image if the collection has this field and we have a value
|
|
245
|
+
const collectionSchema = manifest.collections[collection];
|
|
246
|
+
const hasFeaturedImageField = collectionSchema?.fields
|
|
247
|
+
? "featured_image" in collectionSchema.fields
|
|
248
|
+
: false;
|
|
249
|
+
if (hasFeaturedImageField) {
|
|
250
|
+
const thumbnailId = post.meta.get("_thumbnail_id");
|
|
251
|
+
const featuredImage = thumbnailId ? attachmentMap.get(String(thumbnailId)) : undefined;
|
|
252
|
+
if (featuredImage) {
|
|
253
|
+
data.featured_image = featuredImage;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Resolve author ID from mappings
|
|
258
|
+
let authorId: string | undefined;
|
|
259
|
+
if (config.authorMappings && post.creator) {
|
|
260
|
+
const mappedUserId = config.authorMappings[post.creator];
|
|
261
|
+
if (mappedUserId !== undefined && mappedUserId !== null) {
|
|
262
|
+
authorId = mappedUserId;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const bylineId = await resolveImportByline(
|
|
267
|
+
post.creator,
|
|
268
|
+
authorDisplayNames?.get(post.creator ?? "") ?? post.creator,
|
|
269
|
+
authorId,
|
|
270
|
+
bylineRepo,
|
|
271
|
+
bylineCache,
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
// Preserve original WordPress dates using the shared WXR date parser.
|
|
275
|
+
// Fallback chain: postDateGmt (UTC) → pubDate (RFC 2822) → postDate (site-local).
|
|
276
|
+
const parsedDate = parseWxrDate(post.postDateGmt, post.pubDate, post.postDate);
|
|
277
|
+
const createdAt = parsedDate ? parsedDate.toISOString() : undefined;
|
|
278
|
+
const publishedAt = status === "published" && createdAt ? createdAt : undefined;
|
|
279
|
+
|
|
280
|
+
// Create the content item
|
|
281
|
+
const createResult = await dineway.handleContentCreate(collection, {
|
|
282
|
+
data,
|
|
283
|
+
slug,
|
|
284
|
+
status,
|
|
285
|
+
authorId,
|
|
286
|
+
bylines: bylineId ? [{ bylineId }] : undefined,
|
|
287
|
+
locale,
|
|
288
|
+
createdAt,
|
|
289
|
+
publishedAt,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
if (createResult.success) {
|
|
293
|
+
result.imported++;
|
|
294
|
+
result.byCollection[collection] = (result.byCollection[collection] || 0) + 1;
|
|
295
|
+
} else {
|
|
296
|
+
result.errors.push({
|
|
297
|
+
title: post.title || "Untitled",
|
|
298
|
+
error:
|
|
299
|
+
typeof createResult.error === "object" && createResult.error !== null
|
|
300
|
+
? (createResult.error as { message?: string }).message || "Unknown error"
|
|
301
|
+
: String(createResult.error),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.error(`Import error for "${post.title || "Untitled"}":`, error);
|
|
306
|
+
result.errors.push({
|
|
307
|
+
title: post.title || "Untitled",
|
|
308
|
+
error: error instanceof Error && error.message ? error.message : "Failed to import item",
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
result.success = result.errors.length === 0;
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function mapStatus(wpStatus: string | undefined): string {
|
|
318
|
+
switch (wpStatus) {
|
|
319
|
+
case "publish":
|
|
320
|
+
return "published";
|
|
321
|
+
case "draft":
|
|
322
|
+
return "draft";
|
|
323
|
+
case "pending":
|
|
324
|
+
return "draft";
|
|
325
|
+
case "private":
|
|
326
|
+
return "draft";
|
|
327
|
+
default:
|
|
328
|
+
return "draft";
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress media import endpoint
|
|
3
|
+
*
|
|
4
|
+
* POST /_dineway/api/import/wordpress/media
|
|
5
|
+
*
|
|
6
|
+
* Downloads media attachments from WordPress URLs and uploads to Dineway storage.
|
|
7
|
+
* Streams progress updates as newline-delimited JSON (NDJSON).
|
|
8
|
+
* Each line is either a progress update or the final result.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
|
|
13
|
+
import type { APIRoute } from "astro";
|
|
14
|
+
import { MediaRepository, computeContentHash } from "dineway";
|
|
15
|
+
import mime from "mime/lite";
|
|
16
|
+
import { ulid } from "ulidx";
|
|
17
|
+
|
|
18
|
+
import { requirePerm } from "#api/authorize.js";
|
|
19
|
+
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
20
|
+
import { isParseError, parseBody } from "#api/parse.js";
|
|
21
|
+
import { wpMediaImportBody } from "#api/schemas.js";
|
|
22
|
+
import { validateExternalUrl, ssrfSafeFetch, SsrfError } from "#import/ssrf.js";
|
|
23
|
+
import type { DinewayHandlers } from "#types";
|
|
24
|
+
|
|
25
|
+
import type { AttachmentInfo } from "./analyze.js";
|
|
26
|
+
|
|
27
|
+
export const prerender = false;
|
|
28
|
+
|
|
29
|
+
/** Progress update sent during streaming */
|
|
30
|
+
export interface MediaImportProgress {
|
|
31
|
+
type: "progress";
|
|
32
|
+
current: number;
|
|
33
|
+
total: number;
|
|
34
|
+
filename?: string;
|
|
35
|
+
status: "downloading" | "uploading" | "done" | "skipped" | "failed";
|
|
36
|
+
error?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Final result sent at end of stream */
|
|
40
|
+
export interface MediaImportResult {
|
|
41
|
+
type?: "result";
|
|
42
|
+
/** Successfully imported items */
|
|
43
|
+
imported: Array<{
|
|
44
|
+
wpId?: number;
|
|
45
|
+
originalUrl: string;
|
|
46
|
+
newUrl: string;
|
|
47
|
+
mediaId: string;
|
|
48
|
+
}>;
|
|
49
|
+
/** Failed items */
|
|
50
|
+
failed: Array<{
|
|
51
|
+
wpId?: number;
|
|
52
|
+
originalUrl: string;
|
|
53
|
+
error: string;
|
|
54
|
+
}>;
|
|
55
|
+
/** Map of old URLs to new URLs (for content rewriting) */
|
|
56
|
+
urlMap: Record<string, string>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const POST: APIRoute = async ({ request, locals }) => {
|
|
60
|
+
const { dineway, user } = locals;
|
|
61
|
+
|
|
62
|
+
const denied = requirePerm(user, "import:execute");
|
|
63
|
+
if (denied) return denied;
|
|
64
|
+
|
|
65
|
+
if (!dineway?.storage) {
|
|
66
|
+
return apiError("NO_STORAGE", "Storage not configured. Media import requires storage.", 501);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!dineway?.db) {
|
|
70
|
+
return apiError("NO_DB", "Database not initialized", 500);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const body = await parseBody(request, wpMediaImportBody);
|
|
75
|
+
if (isParseError(body)) return body;
|
|
76
|
+
|
|
77
|
+
const attachments = body.attachments as AttachmentInfo[];
|
|
78
|
+
|
|
79
|
+
// Check if streaming is requested (default: true)
|
|
80
|
+
const shouldStream = body.stream !== false;
|
|
81
|
+
|
|
82
|
+
if (shouldStream) {
|
|
83
|
+
// Stream progress updates as NDJSON
|
|
84
|
+
const stream = new ReadableStream({
|
|
85
|
+
async start(controller) {
|
|
86
|
+
const encoder = new TextEncoder();
|
|
87
|
+
const sendProgress = (progress: MediaImportProgress) => {
|
|
88
|
+
controller.enqueue(encoder.encode(JSON.stringify(progress) + "\n"));
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const result = await importMediaWithProgress(
|
|
92
|
+
attachments,
|
|
93
|
+
dineway.db,
|
|
94
|
+
dineway.storage,
|
|
95
|
+
request.url,
|
|
96
|
+
sendProgress,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Send final result
|
|
100
|
+
controller.enqueue(encoder.encode(JSON.stringify({ ...result, type: "result" }) + "\n"));
|
|
101
|
+
controller.close();
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
return new Response(stream, {
|
|
106
|
+
status: 200,
|
|
107
|
+
headers: {
|
|
108
|
+
"Content-Type": "application/x-ndjson",
|
|
109
|
+
"Cache-Control": "private, no-store",
|
|
110
|
+
"Transfer-Encoding": "chunked",
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Non-streaming mode
|
|
116
|
+
const result = await importMediaWithProgress(
|
|
117
|
+
attachments,
|
|
118
|
+
dineway.db,
|
|
119
|
+
dineway.storage,
|
|
120
|
+
request.url,
|
|
121
|
+
() => {}, // No-op progress callback
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
return apiSuccess(result);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
return handleError(error, "Failed to import media", "IMPORT_ERROR");
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
async function importMediaWithProgress(
|
|
131
|
+
attachments: AttachmentInfo[],
|
|
132
|
+
db: NonNullable<DinewayHandlers["db"]>,
|
|
133
|
+
storage: NonNullable<DinewayHandlers["storage"]>,
|
|
134
|
+
requestUrl: string,
|
|
135
|
+
onProgress: (progress: MediaImportProgress) => void,
|
|
136
|
+
): Promise<MediaImportResult> {
|
|
137
|
+
const repo = new MediaRepository(db);
|
|
138
|
+
const url = new URL(requestUrl);
|
|
139
|
+
const baseUrl = `${url.protocol}//${url.host}`;
|
|
140
|
+
const total = attachments.length;
|
|
141
|
+
|
|
142
|
+
const result: MediaImportResult = {
|
|
143
|
+
imported: [],
|
|
144
|
+
failed: [],
|
|
145
|
+
urlMap: {},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
for (let i = 0; i < attachments.length; i++) {
|
|
149
|
+
const attachment = attachments[i];
|
|
150
|
+
const current = i + 1;
|
|
151
|
+
const filename = attachment.filename || `file-${attachment.id}`;
|
|
152
|
+
|
|
153
|
+
if (!attachment.url) {
|
|
154
|
+
result.failed.push({
|
|
155
|
+
wpId: attachment.id,
|
|
156
|
+
originalUrl: "",
|
|
157
|
+
error: "No URL provided",
|
|
158
|
+
});
|
|
159
|
+
onProgress({
|
|
160
|
+
type: "progress",
|
|
161
|
+
current,
|
|
162
|
+
total,
|
|
163
|
+
filename,
|
|
164
|
+
status: "failed",
|
|
165
|
+
error: "No URL provided",
|
|
166
|
+
});
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
// SSRF: validate URL before fetching
|
|
172
|
+
try {
|
|
173
|
+
validateExternalUrl(attachment.url);
|
|
174
|
+
} catch (e) {
|
|
175
|
+
const msg = e instanceof SsrfError ? e.message : "Invalid URL";
|
|
176
|
+
result.failed.push({
|
|
177
|
+
wpId: attachment.id,
|
|
178
|
+
originalUrl: attachment.url,
|
|
179
|
+
error: `Blocked: ${msg}`,
|
|
180
|
+
});
|
|
181
|
+
onProgress({
|
|
182
|
+
type: "progress",
|
|
183
|
+
current,
|
|
184
|
+
total,
|
|
185
|
+
filename,
|
|
186
|
+
status: "failed",
|
|
187
|
+
error: `Blocked: ${msg}`,
|
|
188
|
+
});
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Report downloading
|
|
193
|
+
onProgress({
|
|
194
|
+
type: "progress",
|
|
195
|
+
current,
|
|
196
|
+
total,
|
|
197
|
+
filename,
|
|
198
|
+
status: "downloading",
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Download from WordPress (ssrfSafeFetch re-validates redirect targets)
|
|
202
|
+
const response = await ssrfSafeFetch(attachment.url, {
|
|
203
|
+
headers: {
|
|
204
|
+
"User-Agent": "Dineway-Importer/1.0",
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (!response.ok) {
|
|
209
|
+
result.failed.push({
|
|
210
|
+
wpId: attachment.id,
|
|
211
|
+
originalUrl: attachment.url,
|
|
212
|
+
error: `HTTP ${response.status}: ${response.statusText}`,
|
|
213
|
+
});
|
|
214
|
+
onProgress({
|
|
215
|
+
type: "progress",
|
|
216
|
+
current,
|
|
217
|
+
total,
|
|
218
|
+
filename,
|
|
219
|
+
status: "failed",
|
|
220
|
+
error: `HTTP ${response.status}`,
|
|
221
|
+
});
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Get content type from response or guess from filename
|
|
226
|
+
const contentType =
|
|
227
|
+
response.headers.get("content-type") || attachment.mimeType || "application/octet-stream";
|
|
228
|
+
|
|
229
|
+
// Get the file data
|
|
230
|
+
const buffer = await response.arrayBuffer();
|
|
231
|
+
const size = buffer.byteLength;
|
|
232
|
+
|
|
233
|
+
// Compute content hash for deduplication
|
|
234
|
+
const contentHash = await computeContentHash(buffer);
|
|
235
|
+
|
|
236
|
+
// Check if we already have this exact content
|
|
237
|
+
const existing = await repo.findByContentHash(contentHash);
|
|
238
|
+
if (existing) {
|
|
239
|
+
// Same content already exists - reuse it
|
|
240
|
+
const existingUrl = `${baseUrl}/_dineway/api/media/file/${existing.storageKey}`;
|
|
241
|
+
result.urlMap[attachment.url] = existingUrl;
|
|
242
|
+
result.imported.push({
|
|
243
|
+
wpId: attachment.id,
|
|
244
|
+
originalUrl: attachment.url,
|
|
245
|
+
newUrl: existingUrl,
|
|
246
|
+
mediaId: existing.id,
|
|
247
|
+
});
|
|
248
|
+
onProgress({
|
|
249
|
+
type: "progress",
|
|
250
|
+
current,
|
|
251
|
+
total,
|
|
252
|
+
filename,
|
|
253
|
+
status: "skipped",
|
|
254
|
+
});
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Report uploading
|
|
259
|
+
onProgress({
|
|
260
|
+
type: "progress",
|
|
261
|
+
current,
|
|
262
|
+
total,
|
|
263
|
+
filename,
|
|
264
|
+
status: "uploading",
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Generate storage key
|
|
268
|
+
const id = ulid();
|
|
269
|
+
const ext = attachment.filename
|
|
270
|
+
? path.extname(attachment.filename)
|
|
271
|
+
: getExtensionFromMimeType(contentType);
|
|
272
|
+
const storageKey = `${id}${ext}`;
|
|
273
|
+
|
|
274
|
+
// Upload to storage
|
|
275
|
+
await storage.upload({
|
|
276
|
+
key: storageKey,
|
|
277
|
+
body: new Uint8Array(buffer),
|
|
278
|
+
contentType,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Create media record with content hash
|
|
282
|
+
const mediaItem = await repo.create({
|
|
283
|
+
filename: attachment.filename || `media-${attachment.id}${ext}`,
|
|
284
|
+
mimeType: contentType,
|
|
285
|
+
size,
|
|
286
|
+
storageKey,
|
|
287
|
+
contentHash,
|
|
288
|
+
width: undefined,
|
|
289
|
+
height: undefined,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Build the new URL
|
|
293
|
+
const newUrl = `${baseUrl}/_dineway/api/media/file/${storageKey}`;
|
|
294
|
+
|
|
295
|
+
result.imported.push({
|
|
296
|
+
wpId: attachment.id,
|
|
297
|
+
originalUrl: attachment.url,
|
|
298
|
+
newUrl,
|
|
299
|
+
mediaId: mediaItem.id,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Add to URL map
|
|
303
|
+
result.urlMap[attachment.url] = newUrl;
|
|
304
|
+
|
|
305
|
+
// Report done
|
|
306
|
+
onProgress({
|
|
307
|
+
type: "progress",
|
|
308
|
+
current,
|
|
309
|
+
total,
|
|
310
|
+
filename,
|
|
311
|
+
status: "done",
|
|
312
|
+
});
|
|
313
|
+
} catch (error) {
|
|
314
|
+
console.error(`Media import error for "${filename}":`, error);
|
|
315
|
+
const errorMsg = "Failed to import media";
|
|
316
|
+
result.failed.push({
|
|
317
|
+
wpId: attachment.id,
|
|
318
|
+
originalUrl: attachment.url,
|
|
319
|
+
error: errorMsg,
|
|
320
|
+
});
|
|
321
|
+
onProgress({
|
|
322
|
+
type: "progress",
|
|
323
|
+
current,
|
|
324
|
+
total,
|
|
325
|
+
filename,
|
|
326
|
+
status: "failed",
|
|
327
|
+
error: errorMsg,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function getExtensionFromMimeType(mimeType: string): string {
|
|
336
|
+
const ext = mime.getExtension(mimeType);
|
|
337
|
+
return ext ? `.${ext}` : "";
|
|
338
|
+
}
|