11/* eslint-disable @typescript-eslint/naming-convention */
2- import { FrontMatterCache , Notice , TFile } from "obsidian" ;
2+ import { Notice , TFile } from "obsidian" ;
3+ import { addFile } from "@repo/database/lib/files" ;
4+ import mime from "mime-types" ;
35import { ensureNodeInstanceId } from "~/utils/nodeInstanceId" ;
46import type { DGSupabaseClient } from "@repo/database/lib/client" ;
57import type { Json } from "@repo/database/dbTypes" ;
@@ -9,7 +11,7 @@ import {
911 type SupabaseContext ,
1012} from "./supabaseContext" ;
1113import { default as DiscourseGraphPlugin } from "~/index" ;
12- import { publishNode , ensurePublishedRelationsAccuracy } from "./publishNode" ;
14+ import { ensurePublishedRelationsAccuracy } from "./publishNode" ;
1315import { upsertNodesToSupabaseAsContentWithEmbeddings } from "./upsertNodesAsContentWithEmbeddings" ;
1416import {
1517 orderConceptsByDependency ,
@@ -579,6 +581,92 @@ const convertDgToSupabaseConcepts = async ({
579581 }
580582} ;
581583
584+ export const syncPublishedNodeAssets = async ( {
585+ plugin,
586+ client,
587+ nodeId,
588+ spaceId,
589+ file,
590+ attachments,
591+ } : {
592+ plugin : DiscourseGraphPlugin ;
593+ client : DGSupabaseClient ;
594+ nodeId : string ;
595+ spaceId : number ;
596+ file : TFile ;
597+ attachments ?: TFile [ ] ;
598+ } ) : Promise < void > => {
599+ if ( attachments === undefined ) {
600+ const embeds = plugin . app . metadataCache . getFileCache ( file ) ?. embeds ?? [ ] ;
601+ attachments = embeds
602+ . map ( ( { link } ) => {
603+ const attachment = plugin . app . metadataCache . getFirstLinkpathDest (
604+ link ,
605+ file . path ,
606+ ) ;
607+ return attachment ;
608+ } )
609+ . filter ( ( a ) => ! ! a ) ;
610+ }
611+ // Always sync non-text assets when node is published to this group
612+ const existingFiles : string [ ] = [ ] ;
613+ const existingReferencesReq = await client
614+ . from ( "my_file_references" )
615+ . select ( "*" )
616+ . eq ( "space_id" , spaceId )
617+ . eq ( "source_local_id" , nodeId ) ;
618+ if ( existingReferencesReq . error ) {
619+ console . error ( existingReferencesReq . error ) ;
620+ return ;
621+ }
622+ const existingReferencesByPath = Object . fromEntries (
623+ existingReferencesReq . data . map ( ( ref ) => [ ref . filepath , ref ] ) ,
624+ ) as Record < string , ( typeof existingReferencesReq . data ) [ 0 ] > ;
625+
626+ for ( const attachment of attachments ) {
627+ const mimetype = mime . lookup ( attachment . path ) || "application/octet-stream" ;
628+ if ( mimetype . startsWith ( "text/" ) ) continue ;
629+ // Do not use standard upload for large files
630+ if ( attachment . stat . size >= 6 * 1024 * 1024 ) {
631+ new Notice (
632+ `Asset file ${ attachment . path } is larger than 6Mb and will not be uploaded` ,
633+ ) ;
634+ continue ;
635+ }
636+ existingFiles . push ( attachment . path ) ;
637+ const existingRef = existingReferencesByPath [ attachment . path ] ;
638+ if (
639+ ! existingRef ||
640+ new Date ( existingRef . last_modified + "Z" ) . valueOf ( ) <
641+ attachment . stat . mtime
642+ ) {
643+ const content = await plugin . app . vault . readBinary ( attachment ) ;
644+ await addFile ( {
645+ client,
646+ spaceId,
647+ sourceLocalId : nodeId ,
648+ fname : attachment . path ,
649+ mimetype,
650+ created : new Date ( attachment . stat . ctime ) ,
651+ lastModified : new Date ( attachment . stat . mtime ) ,
652+ content,
653+ } ) ;
654+ }
655+ }
656+ let cleanupCommand = client
657+ . from ( "FileReference" )
658+ . delete ( )
659+ . eq ( "space_id" , spaceId )
660+ . eq ( "source_local_id" , nodeId ) ;
661+ if ( existingFiles . length )
662+ cleanupCommand = cleanupCommand . notIn ( "filepath" , [
663+ ...new Set ( existingFiles ) ,
664+ ] ) ;
665+ const cleanupResult = await cleanupCommand ;
666+ // do not fail on cleanup
667+ if ( cleanupResult . error ) console . error ( cleanupResult . error ) ;
668+ } ;
669+
582670/**
583671 * For nodes that are already published, ensure non-text assets are pushed to
584672 * storage. Called after content sync so new embeds (e.g. images) get uploaded.
@@ -587,17 +675,26 @@ const syncPublishedNodesAssets = async (
587675 plugin : DiscourseGraphPlugin ,
588676 nodes : ObsidianDiscourseNodeData [ ] ,
589677) : Promise < void > => {
678+ const context = await getSupabaseContext ( plugin ) ;
679+ if ( ! context ) throw new Error ( "Cannot get context" ) ;
680+ const spaceId = context . spaceId ;
681+ const client = await getLoggedInClient ( plugin ) ;
682+ if ( ! client ) throw new Error ( "Cannot get client" ) ;
590683 const published = nodes . filter (
591684 ( n ) =>
592685 ( ( n . frontmatter . publishedToGroups as string [ ] | undefined ) ?. length ?? 0 ) >
593686 0 ,
594687 ) ;
595688 for ( const node of published ) {
596689 try {
597- await publishNode ( {
690+ const nodeId = node . frontmatter . nodeInstanceId as string | undefined ;
691+ if ( ! nodeId ) throw new Error ( "Please sync the node first" ) ;
692+ await syncPublishedNodeAssets ( {
598693 plugin,
694+ client,
695+ nodeId,
696+ spaceId,
599697 file : node . file ,
600- frontmatter : node . frontmatter as FrontMatterCache ,
601698 } ) ;
602699 } catch ( error ) {
603700 console . error (
@@ -651,7 +748,12 @@ const syncChangedNodesToSupabase = async ({
651748
652749 // When file changes affect an already-published node, ensure new non-text
653750 // assets (e.g. images) are pushed to storage.
654- await syncPublishedNodesAssets ( plugin , changedNodes ) ;
751+ try {
752+ await syncPublishedNodesAssets ( plugin , changedNodes ) ;
753+ } catch ( error ) {
754+ console . error ( `Failed to sync published node assets` , error ) ;
755+ new Notice ( `Failed to sync published node assets.` ) ;
756+ }
655757} ;
656758
657759/**
0 commit comments