Skip to content

Commit 1f5d478

Browse files
committed
fix: resolve WhatsApp media copy paths
1 parent fb8810d commit 1f5d478

3 files changed

Lines changed: 100 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010

1111
- Update `crawlkit` to v0.7.0.
1212

13+
### Fixed
14+
15+
- Resolve WhatsApp Desktop `Media/...` paths through `Message/Media` before copying archive media (#9, thanks @pasogott).
16+
1317
## [0.2.5] - 2026-05-15
1418

1519
### Changed

internal/whatsappdb/desktop.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,7 @@ order by m.ZMESSAGEDATE asc, m.Z_PK asc`)
576576
m.MediaType = mediaType(m.RawType)
577577
m.MediaTitle = firstNonEmpty(mediaTitle, vcardName)
578578
if mediaPath != "" {
579-
m.MediaPath = filepath.Join(sourceRoot, filepath.FromSlash(mediaPath))
579+
m.MediaPath = resolveDesktopMediaPath(sourceRoot, mediaPath)
580580
}
581581
m.MediaURL = mediaURL
582582
m.SenderJID, m.SenderName = sender(m.FromMe, m.ChatJID, fromJID, toJID, pushName, memberJID, memberName, memberFirst, names)
@@ -591,6 +591,57 @@ order by m.ZMESSAGEDATE asc, m.Z_PK asc`)
591591
return out, mediaCount, rows.Err()
592592
}
593593

594+
func resolveDesktopMediaPath(sourceRoot, dbPath string) string {
595+
dbPath = strings.TrimSpace(dbPath)
596+
if dbPath == "" {
597+
return ""
598+
}
599+
rel := cleanDesktopMediaRel(filepath.FromSlash(dbPath))
600+
if rel == "" {
601+
return ""
602+
}
603+
primary := filepath.Join(sourceRoot, rel)
604+
if firstPathElement(rel) != "Media" {
605+
return primary
606+
}
607+
messageMedia := filepath.Join(sourceRoot, "Message", rel)
608+
if _, err := os.Stat(messageMedia); err == nil {
609+
return messageMedia
610+
}
611+
if _, err := os.Stat(primary); err == nil {
612+
return primary
613+
}
614+
return messageMedia
615+
}
616+
617+
func cleanDesktopMediaRel(path string) string {
618+
rel := filepath.Clean(path)
619+
if filepath.IsAbs(rel) {
620+
rel = strings.TrimLeft(rel, string(os.PathSeparator))
621+
rel = filepath.Clean(rel)
622+
}
623+
if rel == "." || rel == ".." {
624+
return ""
625+
}
626+
if strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
627+
rel = filepath.Base(rel)
628+
if rel == "." || rel == ".." {
629+
return ""
630+
}
631+
}
632+
return rel
633+
}
634+
635+
func firstPathElement(path string) string {
636+
if path == "." || path == "" {
637+
return ""
638+
}
639+
if i := strings.IndexRune(path, os.PathSeparator); i >= 0 {
640+
return path[:i]
641+
}
642+
return path
643+
}
644+
594645
func sender(fromMe bool, chatJID, fromJID, toJID, pushName, memberJID, memberName, memberFirst string, names map[string]string) (string, string) {
595646
if fromMe {
596647
return firstNonEmpty(toJID), "me"

internal/whatsappdb/desktop_test.go

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ func TestImportDesktopCopyMedia(t *testing.T) {
191191
ctx := context.Background()
192192
source := t.TempDir()
193193
createFixtureDBs(t, source)
194-
mediaPath := filepath.Join(source, "Media", "123@g.us", "a", "test.jpg")
194+
mediaPath := filepath.Join(source, "Message", "Media", "123@g.us", "a", "test.jpg")
195195
if err := os.MkdirAll(filepath.Dir(mediaPath), 0o700); err != nil {
196196
t.Fatal(err)
197197
}
@@ -239,19 +239,60 @@ insert into ZWAMESSAGE values (5, 2, 1, 2, 'missing-media', 0, 700000004, 'missi
239239
missingPath = msg.MediaPath
240240
}
241241
}
242-
wantCopied := filepath.Join(filepath.Dir(archivePath), "media", "Media", "123@g.us", "a", "test.jpg")
242+
wantCopied := filepath.Join(filepath.Dir(archivePath), "media", "Message", "Media", "123@g.us", "a", "test.jpg")
243243
if copiedPath != wantCopied {
244244
t.Fatalf("copied media path = %q, want %q", copiedPath, wantCopied)
245245
}
246246
if data, err := os.ReadFile(copiedPath); err != nil || string(data) != "image" { // #nosec G304 -- copiedPath is asserted against the expected temp archive path above.
247247
t.Fatalf("copied media content = %q err=%v", data, err)
248248
}
249-
wantMissing := filepath.Join(source, "Media", "123@g.us", "a", "missing.jpg")
249+
wantMissing := filepath.Join(source, "Message", "Media", "123@g.us", "a", "missing.jpg")
250250
if missingPath != wantMissing {
251251
t.Fatalf("missing media path = %q, want original %q", missingPath, wantMissing)
252252
}
253253
}
254254

255+
func TestResolveDesktopMediaPathPrefersMessageMedia(t *testing.T) {
256+
source := t.TempDir()
257+
messageMedia := filepath.Join(source, "Message", "Media", "chat", "photo.jpg")
258+
if err := os.MkdirAll(filepath.Dir(messageMedia), 0o700); err != nil {
259+
t.Fatal(err)
260+
}
261+
if err := os.WriteFile(messageMedia, []byte("image"), 0o600); err != nil {
262+
t.Fatal(err)
263+
}
264+
if got := resolveDesktopMediaPath(source, "Media/chat/photo.jpg"); got != messageMedia {
265+
t.Fatalf("resolved media path = %q, want %q", got, messageMedia)
266+
}
267+
268+
legacyMedia := filepath.Join(source, "Media", "chat", "legacy.jpg")
269+
if err := os.MkdirAll(filepath.Dir(legacyMedia), 0o700); err != nil {
270+
t.Fatal(err)
271+
}
272+
if err := os.WriteFile(legacyMedia, []byte("image"), 0o600); err != nil {
273+
t.Fatal(err)
274+
}
275+
if got := resolveDesktopMediaPath(source, "Media/chat/legacy.jpg"); got != legacyMedia {
276+
t.Fatalf("legacy media path = %q, want %q", got, legacyMedia)
277+
}
278+
279+
missing := filepath.Join(source, "Message", "Media", "chat", "missing.jpg")
280+
if got := resolveDesktopMediaPath(source, "Media/chat/missing.jpg"); got != missing {
281+
t.Fatalf("missing media path = %q, want %q", got, missing)
282+
}
283+
284+
absolute := filepath.Join(string(os.PathSeparator), "tmp", "outside.jpg")
285+
confined := filepath.Join(source, "tmp", "outside.jpg")
286+
if got := resolveDesktopMediaPath(source, absolute); got != confined {
287+
t.Fatalf("absolute media path = %q, want confined %q", got, confined)
288+
}
289+
290+
traversal := filepath.Join(source, "outside.jpg")
291+
if got := resolveDesktopMediaPath(source, "../outside.jpg"); got != traversal {
292+
t.Fatalf("traversal media path = %q, want confined %q", got, traversal)
293+
}
294+
}
295+
255296
func TestCopyArchiveMediaDeduplicatesAndConfinesPaths(t *testing.T) {
256297
source := t.TempDir()
257298
mediaRoot := filepath.Join(t.TempDir(), "media")

0 commit comments

Comments
 (0)