feat(media): images and recordings in backups, exports and imports (M6)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m52s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m52s
- Backup ZIP (download and automatic per-user server backups) carries the files of the user's own notes as media/<id><ext>; restoring creates them with new IDs and rewrites the blocks. - Markdown archive puts them under attachments/ and embeds them relative to each note (); re-importing turns them back into image/audio blocks. - Obsidian import takes images and audio a note embeds on its own (![[file.png]], ); unused or invalid files appear as skipped in the preview instead of vanishing. - Format detection and file paths moved into storage for this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3f2bc96732
commit
1f63e08a73
13 files changed
+608
-82
No files matched your search
@@ -32,15 +32,36 @@ func (s *Store) WriteMarkdownVault(userID string, w io.Writer) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Images and recordings go to attachments/, notes link them relative to their folder
|
||||
attachments := map[string]string{} // media ID -> path in the archive
|
||||
if media, err := s.MediaOfUser(userID); err == nil {
|
||||
for _, m := range media {
|
||||
attachments[m.ID] = vaultAttachmentDir + "/" + m.ID + MediaExt(m.Mime)
|
||||
}
|
||||
}
|
||||
if err := s.writeMediaFiles(zw, userID, func(m Media) string { return attachments[m.ID] }); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
docs, err := s.GetDocuments(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, d := range docs {
|
||||
name := names.file(names.dir(d.FolderPath), d.Title, "Unbenannte Notiz")
|
||||
dir := names.dir(d.FolderPath)
|
||||
name := names.file(dir, d.Title, "Unbenannte Notiz")
|
||||
up := ""
|
||||
if dir != "" {
|
||||
up = strings.Repeat("../", strings.Count(dir, "/")+1)
|
||||
}
|
||||
body := markdownFrontmatter(map[string]any{
|
||||
"title": d.Title, "icon": d.Icon, "tags": d.Tags, "created_at": d.CreatedAt, "updated_at": d.UpdatedAt,
|
||||
}) + BlocksToVaultMarkdown(d.Blocks)
|
||||
}) + blocksToVaultMarkdown(d.Blocks, func(id string) string {
|
||||
if p, ok := attachments[id]; ok {
|
||||
return up + p
|
||||
}
|
||||
return ""
|
||||
})
|
||||
if err := writeZipFile(zw, name, body, parseISO(d.UpdatedAt, now)); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -164,7 +185,16 @@ func taskMarkdownLine(t domain.Thought) string {
|
||||
// BlocksToVaultMarkdown turns outliner blocks into Markdown that Obsidian renders natively:
|
||||
// top-level headings, dividers and callouts as real block elements, everything else as a nested
|
||||
// list (one tab per level), code as fenced blocks inside the list.
|
||||
// vaultAttachmentDir holds images and recordings in the Markdown archive.
|
||||
const vaultAttachmentDir = "attachments"
|
||||
|
||||
func BlocksToVaultMarkdown(blocks []domain.DocumentBlock) string {
|
||||
return blocksToVaultMarkdown(blocks, nil)
|
||||
}
|
||||
|
||||
// blocksToVaultMarkdown renders blocks as Markdown; attachment maps a media ID to its relative path
|
||||
// (image and audio blocks become embeds Obsidian shows inline).
|
||||
func blocksToVaultMarkdown(blocks []domain.DocumentBlock, attachment func(id string) string) string {
|
||||
var sb strings.Builder
|
||||
// Children of a top-level heading/callout/code/divider become a list one level up; otherwise their
|
||||
// tab indent would turn them into an indented code block in Markdown
|
||||
@@ -178,6 +208,13 @@ func BlocksToVaultMarkdown(blocks []domain.DocumentBlock) string {
|
||||
}
|
||||
indent := strings.Repeat("\t", lvl)
|
||||
content := strings.ReplaceAll(b.Content, "\r\n", "\n")
|
||||
if (b.Type == "image" || b.Type == "audio") && attachment != nil {
|
||||
if ref, ok := ParseMediaContent(content); ok {
|
||||
if p := attachment(ref.ID); p != "" {
|
||||
content = " + ")"
|
||||
}
|
||||
}
|
||||
}
|
||||
// Soft line breaks continue the list item, aligned under its text
|
||||
cont := func(text, prefix string) string {
|
||||
return strings.ReplaceAll(text, "\n", "\n"+indent+prefix)
|
||||
|
||||
Reference in new issue
Block a user