-
Notifications
You must be signed in to change notification settings - Fork 4
ENG-1652 Add .dg.metadata for stable import folder identification #970
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
trangdoan982
wants to merge
4
commits into
main
Choose a base branch
from
eng-1652-adding-source-vault-identifier-metadata-to-imported-folder
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
1432131
ENG-1652 Add .dg.metadata for stable import folder identification
trangdoan982 2a6db1a
Keep writing spaceNames to settings for UI display
trangdoan982 c9cc256
Apply suggestion from @devin-ai-integration[bot]
trangdoan982 1698399
address PR comment
trangdoan982 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| import { DataAdapter } from "obsidian"; | ||
| import type DiscourseGraphPlugin from "~/index"; | ||
| import type { ImportFolderMetadata } from "~/types"; | ||
|
|
||
| const DG_METADATA_FILE = ".dg.metadata"; | ||
| const IMPORT_ROOT = "import"; | ||
|
|
||
| const sanitizeFileName = (fileName: string): string => { | ||
| return fileName | ||
| .replace(/[<>:"/\\|?*]/g, "") | ||
| .replace(/\s+/g, " ") | ||
| .trim(); | ||
| }; | ||
|
|
||
| const generateShortId = (): string => Math.random().toString(36).slice(2, 8); | ||
|
|
||
| const readImportFolderMetadata = async ( | ||
| adapter: DataAdapter, | ||
| folderPath: string, | ||
| ): Promise<ImportFolderMetadata | null> => { | ||
| const metadataPath = `${folderPath}/${DG_METADATA_FILE}`; | ||
| try { | ||
| const exists = await adapter.exists(metadataPath); | ||
| if (!exists) return null; | ||
|
|
||
| const raw = await adapter.read(metadataPath); | ||
| const parsed: unknown = JSON.parse(raw); | ||
|
|
||
| if ( | ||
| parsed !== null && | ||
| typeof parsed === "object" && | ||
| "spaceUri" in parsed && | ||
| typeof (parsed as Record<string, unknown>).spaceUri === "string" | ||
| ) { | ||
| return parsed as ImportFolderMetadata; | ||
| } | ||
|
|
||
| return null; | ||
| } catch { | ||
| return null; | ||
| } | ||
| }; | ||
|
|
||
| const writeImportFolderMetadata = async ({ | ||
| adapter, | ||
| folderPath, | ||
| metadata, | ||
| }: { | ||
| adapter: DataAdapter; | ||
| folderPath: string; | ||
| metadata: ImportFolderMetadata; | ||
| }): Promise<void> => { | ||
| const metadataPath = `${folderPath}/${DG_METADATA_FILE}`; | ||
| await adapter.write(metadataPath, JSON.stringify(metadata, null, 2)); | ||
| }; | ||
|
|
||
| const buildSpaceUriToFolderMap = async ( | ||
| adapter: DataAdapter, | ||
| ): Promise<Map<string, string>> => { | ||
| const map = new Map<string, string>(); | ||
|
|
||
| const importExists = await adapter.exists(IMPORT_ROOT); | ||
| if (!importExists) return map; | ||
|
|
||
| const { folders } = await adapter.list(IMPORT_ROOT); | ||
|
|
||
| for (const folderPath of folders) { | ||
| const metadata = await readImportFolderMetadata(adapter, folderPath); | ||
| if (!metadata) continue; | ||
|
|
||
| if (map.has(metadata.spaceUri)) { | ||
| console.warn( | ||
| `[importFolderMetadata] Duplicate spaceUri "${metadata.spaceUri}" found in "${folderPath}". Using first occurrence.`, | ||
| ); | ||
| } else { | ||
| map.set(metadata.spaceUri, folderPath); | ||
| } | ||
| } | ||
|
|
||
| return map; | ||
| }; | ||
|
|
||
| export const resolveFolderForSpaceUri = async ({ | ||
| adapter, | ||
| spaceUri, | ||
| spaceName, | ||
| }: { | ||
| adapter: DataAdapter; | ||
| spaceUri: string; | ||
| spaceName: string; | ||
| }): Promise<string> => { | ||
| const spaceUriToFolder = await buildSpaceUriToFolderMap(adapter); | ||
|
|
||
| // 1. Exact spaceUri match | ||
| if (spaceUriToFolder.has(spaceUri)) { | ||
| const folderPath = spaceUriToFolder.get(spaceUri)!; | ||
| const existingMetadata = await readImportFolderMetadata( | ||
| adapter, | ||
| folderPath, | ||
| ); | ||
| if (existingMetadata && existingMetadata.spaceName !== spaceName) { | ||
| await writeImportFolderMetadata({ | ||
| adapter, | ||
| folderPath, | ||
| metadata: { ...existingMetadata, spaceName }, | ||
| }); | ||
| } | ||
| return folderPath; | ||
| } | ||
|
|
||
| // 2. Fallback: scan for a folder whose basename matches the sanitized spaceName | ||
| // but has no metadata yet | ||
| const { folders } = (await adapter.exists(IMPORT_ROOT)) | ||
| ? await adapter.list(IMPORT_ROOT) | ||
| : { folders: [] }; | ||
|
|
||
| const sanitized = sanitizeFileName(spaceName); | ||
|
|
||
| for (const folderPath of folders) { | ||
| const basename = folderPath.split("/").pop(); | ||
| if (basename === sanitized) { | ||
| const existingMetadata = await readImportFolderMetadata( | ||
| adapter, | ||
| folderPath, | ||
| ); | ||
| if (!existingMetadata) { | ||
| await writeImportFolderMetadata({ | ||
| adapter, | ||
| folderPath, | ||
| metadata: { spaceUri, spaceName }, | ||
| }); | ||
| return folderPath; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // 3. Create a new folder, handling name collisions | ||
| const desiredPath = `${IMPORT_ROOT}/${sanitized}`; | ||
| const desiredExists = await adapter.exists(desiredPath); | ||
|
|
||
| let newPath: string; | ||
| if (desiredExists) { | ||
| // The existing folder has a different spaceUri (would have been returned above otherwise) | ||
| newPath = `${IMPORT_ROOT}/${sanitized}-${generateShortId()}`; | ||
| } else { | ||
| newPath = desiredPath; | ||
| } | ||
|
|
||
| await adapter.mkdir(newPath); | ||
| await writeImportFolderMetadata({ | ||
| adapter, | ||
| folderPath: newPath, | ||
| metadata: { spaceUri, spaceName }, | ||
| }); | ||
|
|
||
| return newPath; | ||
| }; | ||
|
|
||
| export const migrateImportFolderMetadata = async ( | ||
| plugin: DiscourseGraphPlugin, | ||
| ): Promise<void> => { | ||
| const adapter = plugin.app.vault.adapter; | ||
|
|
||
| const importExists = await adapter.exists(IMPORT_ROOT); | ||
| if (!importExists) return; | ||
|
|
||
| const { folders } = await adapter.list(IMPORT_ROOT); | ||
|
|
||
| // Invert spaceNames: Record<spaceUri, spaceName> → Map<spaceName, spaceUri> | ||
| const spaceNames = plugin.settings.spaceNames ?? {}; | ||
| const nameToSpaceUri = new Map<string, string>(); | ||
| for (const [spaceUri, name] of Object.entries(spaceNames)) { | ||
| nameToSpaceUri.set(sanitizeFileName(name), spaceUri); | ||
| } | ||
|
trangdoan982 marked this conversation as resolved.
|
||
|
|
||
| for (const folderPath of folders) { | ||
| const metadataPath = `${folderPath}/${DG_METADATA_FILE}`; | ||
| const metadataExists = await adapter.exists(metadataPath); | ||
| if (metadataExists) continue; | ||
|
|
||
| const basename = folderPath.split("/").pop() ?? ""; | ||
| const spaceUri = nameToSpaceUri.get(basename); | ||
|
|
||
| if (spaceUri) { | ||
| await writeImportFolderMetadata({ | ||
| adapter, | ||
| folderPath, | ||
| metadata: { spaceUri, spaceName: spaceNames[spaceUri] ?? basename }, | ||
| }); | ||
| } else { | ||
| console.debug( | ||
| `[importFolderMetadata] No spaceUri found for folder "${basename}", skipping migration.`, | ||
| ); | ||
| } | ||
| } | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ import { | |
| importRelationsForImportedNodes, | ||
| type RemoteRelationInstance, | ||
| } from "./importRelations"; | ||
| import { resolveFolderForSpaceUri } from "./importFolderMetadata"; | ||
|
|
||
| export const getAvailableGroupIds = async ( | ||
| client: DGSupabaseClient, | ||
|
|
@@ -754,15 +755,15 @@ const importAssetsForNode = async ({ | |
| client, | ||
| spaceId, | ||
| nodeInstanceId, | ||
| spaceName, | ||
| importBasePath, | ||
| targetMarkdownFile, | ||
| originalNodePath, | ||
| }: { | ||
| plugin: DiscourseGraphPlugin; | ||
| client: DGSupabaseClient; | ||
| spaceId: number; | ||
| nodeInstanceId: string; | ||
| spaceName: string; | ||
| importBasePath: string; | ||
| targetMarkdownFile: TFile; | ||
| /** Source vault path of the note (e.g. from Content metadata filePath). Used to place assets under import/{space}/ relative to note. */ | ||
| originalNodePath?: string; | ||
|
|
@@ -794,8 +795,6 @@ const importAssetsForNode = async ({ | |
| return { success: true, pathMapping, errors }; | ||
| } | ||
|
|
||
| const importBasePath = `import/${sanitizeFileName(spaceName)}`; | ||
|
|
||
| // Get existing asset mappings from frontmatter | ||
| const cache = plugin.app.metadataCache.getFileCache(targetMarkdownFile); | ||
| const frontmatter = (cache?.frontmatter as Record<string, unknown>) || {}; | ||
|
|
@@ -1226,8 +1225,6 @@ export const importSelectedNodes = async ({ | |
|
|
||
| // Process each space | ||
| for (const [spaceId, nodes] of nodesBySpace.entries()) { | ||
| const spaceName = await getSpaceNameFromId(client, spaceId); | ||
| const importFolderPath = `import/${sanitizeFileName(spaceName)}`; | ||
| const spaceUri = spaceUris.get(spaceId); | ||
| if (!spaceUri) { | ||
| for (const _node of nodes) { | ||
|
|
@@ -1238,12 +1235,12 @@ export const importSelectedNodes = async ({ | |
| continue; | ||
| } | ||
|
|
||
| // Ensure the import folder exists | ||
| const folderExists = | ||
| await plugin.app.vault.adapter.exists(importFolderPath); | ||
| if (!folderExists) { | ||
| await plugin.app.vault.createFolder(importFolderPath); | ||
| } | ||
| const spaceName = await getSpaceNameFromId(client, spaceId); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Realizing that this is another case of a database call within a loop. Can you use |
||
| const importFolderPath = await resolveFolderForSpaceUri({ | ||
| adapter: plugin.app.vault.adapter, | ||
| spaceUri, | ||
| spaceName, | ||
| }); | ||
|
|
||
| // Process each node in this space | ||
| for (const node of nodes) { | ||
|
|
@@ -1344,7 +1341,7 @@ export const importSelectedNodes = async ({ | |
| client, | ||
| spaceId, | ||
| nodeInstanceId: node.nodeInstanceId, | ||
| spaceName, | ||
| importBasePath: importFolderPath, | ||
| targetMarkdownFile: processedFile, | ||
| originalNodePath, | ||
| }); | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's think of a resolution step here. I would say delete the older metadata file of the two?