Skip to content

Commit 60ec5fe

Browse files
feat: move Notes assets to managed storage (#873)
* feat(notes): add managed asset storage runtime * feat(notes): store inserted images in managed assets * fix(notes): retry images after cloud hydration * feat(notes): migrate referenced legacy assets * fix(notes): retry asset migration after cloud hydration * refactor(notes): extract asset reference parsing * feat(storage): audit notes assets in vault doctor * feat(storage): explain notes asset doctor warnings * test(storage): cover managed assets in vault moves * docs(notes): document managed image storage
1 parent a369507 commit 60ec5fe

41 files changed

Lines changed: 2570 additions & 94 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/website/documentation/notes/images.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ description: "Embed images in massCode Notes with standard markdown syntax, past
77

88
Use images when a screenshot, diagram, or visual reference belongs next to the note text.
99

10-
Images are rendered directly inside your note from standard markdown image syntax. For local images, paste an image from the clipboard or drag an image file into the editor. massCode saves the file to `notes/assets` in your vault and inserts the markdown for you.
10+
Images are rendered directly inside your note from standard markdown image syntax. For local images, paste an image from the clipboard or drag an image file into the editor. massCode saves new files to `notes/.masscode/assets` in your vault and inserts the markdown for you.
1111

1212
```md
1313
![image-name](masscode://notes-asset/generated-file-name.png)
@@ -19,7 +19,11 @@ You can also use a remote image URL:
1919
![Remote screenshot](https://example.com/screenshot.png)
2020
```
2121

22+
The `masscode://notes-asset/` URL is resolved by massCode. Other Markdown apps may not display these local images directly. When you copy or synchronize Notes between devices, include the whole vault, including the hidden `notes/.masscode` directory. Copying only Markdown files leaves their local images behind.
23+
24+
Vaults with images in the managed `notes/.masscode/assets` path require a massCode version that supports this layout. Older versions may not display these images.
25+
2226
- Paste or drag images in **Editor** and **Live Preview** modes.
2327
- View images in **Live Preview** or **Preview** mode.
2428
- Click an image block in editable modes to reveal and edit its markdown source.
25-
- Supported pasted and dropped formats: `png`, `jpg`, `jpeg`, `gif`, `webp`, `svg`, and `bmp`.
29+
- New pasted and dropped images can use `png`, `jpg`, or `jpeg`.

src/main/i18n/locales/en_US/preferences.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,24 @@
4545
"invalid-frontmatter": "Invalid frontmatter",
4646
"conflicted-copy": "Conflicted copy"
4747
},
48+
"assetWarnings": {
49+
"NOTES_LEGACY_ASSET": {
50+
"message": "{{assetName}} is still stored in the legacy Notes assets folder.",
51+
"recommendation": "Keep the whole vault synchronized and scan again after migration completes."
52+
},
53+
"NOTES_ASSET_MIGRATION_PENDING": {
54+
"message": "{{assetName}} is not available locally for migration inspection.",
55+
"recommendation": "Make the asset available locally, then scan the vault again."
56+
},
57+
"NOTES_ASSET_DESTINATION_CONFLICT": {
58+
"message": "{{assetName}} differs between the legacy and current Notes assets folders.",
59+
"recommendation": "Compare both files and keep the correct copy before removing either one."
60+
},
61+
"NOTES_ASSET_MISSING": {
62+
"message": "{{assetName}} is missing from both Notes assets folders.",
63+
"recommendation": "Restore the asset to the vault or remove its reference from the note."
64+
}
65+
},
4866
"moreWarnings": "+{{count}} more warnings",
4967
"warnings": "Warnings"
5068
}

src/main/i18n/locales/ru_RU/preferences.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,24 @@
4343
"invalid-frontmatter": "Битый frontmatter",
4444
"conflicted-copy": "Конфликтная копия"
4545
},
46+
"assetWarnings": {
47+
"NOTES_LEGACY_ASSET": {
48+
"message": "{{assetName}} всё ещё хранится в старой папке изображений Notes.",
49+
"recommendation": "Синхронизируйте vault целиком и повторите проверку после завершения миграции."
50+
},
51+
"NOTES_ASSET_MIGRATION_PENDING": {
52+
"message": "{{assetName}} недоступен локально для проверки миграции.",
53+
"recommendation": "Сделайте файл доступным локально и повторно проверьте vault."
54+
},
55+
"NOTES_ASSET_DESTINATION_CONFLICT": {
56+
"message": "{{assetName}} отличается в старой и текущей папках изображений Notes.",
57+
"recommendation": "Сравните оба файла и сохраните правильную копию, прежде чем удалять любой из них."
58+
},
59+
"NOTES_ASSET_MISSING": {
60+
"message": "{{assetName}} отсутствует в обеих папках изображений Notes.",
61+
"recommendation": "Восстановите файл в vault или удалите ссылку на него из заметки."
62+
}
63+
},
4664
"moreWarnings": "+{{count}} предупреждений",
4765
"warnings": "Предупреждения"
4866
}

src/main/index.ts

Lines changed: 19 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
/* eslint-disable node/prefer-global/process */
2-
import { readFile } from 'node:fs/promises'
32
import { createRequire } from 'node:module'
43
import path from 'node:path'
54
import { app, BrowserWindow, ipcMain, Menu, protocol, screen } from 'electron'
@@ -10,7 +9,16 @@ import { startThemeWatcher, stopThemeWatcher } from './ipc/handlers/theme'
109
import { validateStoredLicense } from './license'
1110
import { createMainMenu } from './menu/main'
1211
import { isQuitting, setQuitting } from './quitState'
13-
import { startMarkdownWatcher, stopMarkdownWatcher } from './storage'
12+
import {
13+
prepareMarkdownWatcher,
14+
startMarkdownWatcher,
15+
stopMarkdownWatcher,
16+
} from './storage'
17+
import {
18+
getNotesPaths,
19+
resolveNotesAsset,
20+
} from './storage/providers/markdown/notes/runtime'
21+
import { getVaultPath } from './storage/providers/markdown/runtime/paths'
1422
import { ensureFlatSpacesLayout } from './storage/providers/markdown/runtime/spaces'
1523
import { store } from './store'
1624
import { startTasksCleanupScheduler, stopTasksCleanupScheduler } from './tasks'
@@ -146,37 +154,8 @@ else {
146154
const url = new URL(request.url)
147155

148156
if (url.hostname === 'notes-asset') {
149-
const fileName = url.pathname.replace(/^\//, '')
150-
const vaultPath
151-
= (store.preferences.get('storage.vaultPath') as string | null)
152-
|| path.join(
153-
store.preferences.get('storage.rootPath') as string,
154-
'markdown-vault',
155-
)
156-
ensureFlatSpacesLayout(vaultPath)
157-
const filePath = path.join(vaultPath, 'notes', 'assets', fileName)
158-
159-
try {
160-
const data = await readFile(filePath)
161-
const ext = path.extname(fileName).toLowerCase()
162-
const mimeTypes: Record<string, string> = {
163-
'.png': 'image/png',
164-
'.jpg': 'image/jpeg',
165-
'.jpeg': 'image/jpeg',
166-
'.gif': 'image/gif',
167-
'.webp': 'image/webp',
168-
'.svg': 'image/svg+xml',
169-
'.bmp': 'image/bmp',
170-
}
171-
return new Response(data, {
172-
headers: {
173-
'Content-Type': mimeTypes[ext] || 'application/octet-stream',
174-
},
175-
})
176-
}
177-
catch {
178-
return new Response('Not found', { status: 404 })
179-
}
157+
const paths = getNotesPaths(getVaultPath())
158+
return resolveNotesAsset(url.pathname.replace(/^\//, ''), paths)
180159
}
181160

182161
return new Response('Not found', { status: 404 })
@@ -251,6 +230,13 @@ else {
251230
log('Error registering IPC', error)
252231
}
253232

233+
try {
234+
prepareMarkdownWatcher()
235+
}
236+
catch (error) {
237+
log('Error preparing markdown watcher', error)
238+
}
239+
254240
try {
255241
validateStoredLicense()
256242
}

src/main/ipc/handlers/fs.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ import {
1212
writeFileSync,
1313
} from 'fs-extra'
1414
import slash from 'slash'
15+
import {
16+
getNotesPaths,
17+
parseNotesAssetWritePayload,
18+
writeNotesAsset,
19+
} from '../../storage/providers/markdown/notes/runtime'
1520
import { ensureFlatSpacesLayout } from '../../storage/providers/markdown/runtime/spaces'
1621
import { store } from '../../store'
1722

@@ -118,30 +123,25 @@ export function registerFsHandlers() {
118123
})
119124
})
120125

121-
ipcMain.handle('fs:notes-asset', (event, { buffer, ext }) => {
126+
ipcMain.handle('fs:notes-asset', async (event, payload: unknown) => {
127+
const parsedPayload = parseNotesAssetWritePayload(payload)
128+
if (!parsedPayload) {
129+
throw new TypeError('Invalid Notes asset payload')
130+
}
131+
122132
const vaultPath
123133
= (store.preferences.get('storage.vaultPath') as string | null)
124134
|| join(
125135
store.preferences.get('storage.rootPath') as string,
126136
'markdown-vault',
127137
)
128138

129-
return new Promise((resolve, reject) => {
130-
try {
131-
ensureFlatSpacesLayout(vaultPath)
132-
const assetsPath = join(vaultPath, 'notes', 'assets')
133-
const name = `${generateAssetId()}${ext}`
134-
const dest = join(assetsPath, name)
135-
136-
ensureDirSync(assetsPath)
137-
writeFileSync(dest, Buffer.from(buffer))
138-
139-
resolve(`masscode://notes-asset/${name}`)
140-
}
141-
catch (error) {
142-
reject(error)
143-
}
144-
})
139+
ensureFlatSpacesLayout(vaultPath)
140+
return writeNotesAsset(
141+
getNotesPaths(vaultPath),
142+
parsedPayload.buffer,
143+
parsedPayload.ext,
144+
)
145145
})
146146

147147
ipcMain.handle('fs:import-markdown-folder', async () => {

src/main/storage/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
} from './contracts'
66
import {
77
createMarkdownStorageProvider,
8+
prepareMarkdownWatcher,
89
startMarkdownWatcher,
910
stopMarkdownWatcher,
1011
} from './providers/markdown'
@@ -27,4 +28,4 @@ export function useHttpStorage(): HttpStorageProvider {
2728
return httpStorageProvider
2829
}
2930

30-
export { startMarkdownWatcher, stopMarkdownWatcher }
31+
export { prepareMarkdownWatcher, startMarkdownWatcher, stopMarkdownWatcher }

0 commit comments

Comments
 (0)