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
+3
-59
@@ -23,35 +23,9 @@ import (
|
||||
|
||||
const mediaGrace = 24 * time.Hour // uploads not yet placed in a block are kept this long
|
||||
|
||||
// Accepted formats (detected from the content, not from the name): file extension and kind
|
||||
var mediaTypes = map[string]struct{ ext, kind string }{
|
||||
"image/jpeg": {".jpg", "image"},
|
||||
"image/png": {".png", "image"},
|
||||
"image/gif": {".gif", "image"},
|
||||
"image/webp": {".webp", "image"},
|
||||
"audio/webm": {".webm", "audio"},
|
||||
"video/webm": {".webm", "audio"}, // Chrome's audio-only recordings are detected as video/webm
|
||||
"audio/ogg": {".ogg", "audio"},
|
||||
"application/ogg": {".ogg", "audio"},
|
||||
"audio/mpeg": {".mp3", "audio"},
|
||||
"audio/wave": {".wav", "audio"},
|
||||
"audio/wav": {".wav", "audio"},
|
||||
"audio/mp4": {".m4a", "audio"},
|
||||
"video/mp4": {".m4a", "audio"}, // Safari records MP4/AAC
|
||||
"audio/aac": {".aac", "audio"},
|
||||
}
|
||||
func (s *Server) mediaRoot() string { return s.store.MediaRoot() }
|
||||
|
||||
func (s *Server) mediaRoot() string {
|
||||
return filepath.Join(filepath.Dir(s.cfg.DatabasePath), "media")
|
||||
}
|
||||
|
||||
func (s *Server) mediaPath(m *storage.Media) string {
|
||||
ext := ".bin"
|
||||
if t, ok := mediaTypes[m.Mime]; ok {
|
||||
ext = t.ext
|
||||
}
|
||||
return filepath.Join(s.mediaRoot(), filepath.Base(m.DocID), filepath.Base(m.ID)+ext)
|
||||
}
|
||||
func (s *Server) mediaPath(m *storage.Media) string { return s.store.MediaFilePath(m) }
|
||||
|
||||
// mediaLimit is the largest allowed file of a kind in bytes.
|
||||
func (s *Server) mediaLimit(kind string) int64 {
|
||||
@@ -65,37 +39,7 @@ func (s *Server) mediaLimit(kind string) int64 {
|
||||
return int64(mb) << 20
|
||||
}
|
||||
|
||||
// detectMedia recognises the format from the first bytes. Browsers send audio as e.g.
|
||||
// "audio/webm;codecs=opus"; that name is kept when the content looks like a container we accept.
|
||||
func detectMedia(data []byte, declared string) (mime, kind string, ok bool) {
|
||||
sniffed := http.DetectContentType(data)
|
||||
if i := strings.IndexByte(sniffed, ';'); i >= 0 {
|
||||
sniffed = sniffed[:i]
|
||||
}
|
||||
// Go does not know WebP and M4A by name; check their signatures directly
|
||||
switch {
|
||||
case len(data) >= 12 && string(data[0:4]) == "RIFF" && string(data[8:12]) == "WEBP":
|
||||
sniffed = "image/webp"
|
||||
case len(data) >= 12 && string(data[4:8]) == "ftyp":
|
||||
sniffed = "video/mp4"
|
||||
}
|
||||
t, ok := mediaTypes[sniffed]
|
||||
if !ok {
|
||||
return "", "", false
|
||||
}
|
||||
if t.kind == "audio" {
|
||||
if d := strings.ToLower(strings.TrimSpace(strings.SplitN(declared, ";", 2)[0])); strings.HasPrefix(d, "audio/") {
|
||||
if dt, known := mediaTypes[d]; known && dt.kind == "audio" {
|
||||
sniffed = d
|
||||
}
|
||||
} else if sniffed == "video/webm" {
|
||||
sniffed = "audio/webm"
|
||||
} else if sniffed == "video/mp4" {
|
||||
sniffed = "audio/mp4"
|
||||
}
|
||||
}
|
||||
return sniffed, t.kind, true
|
||||
}
|
||||
var detectMedia = storage.DetectMedia
|
||||
|
||||
func (s *Server) canEditDoc(userID, docID string) bool {
|
||||
role := s.store.DocRole(userID, docID)
|
||||
|
||||
Reference in new issue
Block a user