Compare commits

...
2 Commits
2 changed files with 97 additions and 28 deletions

No files matched your search

+77 -28
View File
@@ -60,6 +60,14 @@ pub:
remove_cover bool remove_cover bool
} }
// Как прошла запись: на месте (тот же файл) или новым файлом поверх старого
pub struct WriteReport {
pub:
in_place bool
// Почему не удалось переписать на месте (пусто, если на месте)
fallback string
}
struct ParsedTag { struct ParsedTag {
mut: mut:
version u8 version u8
@@ -558,30 +566,74 @@ fn build_tag(parsed ParsedTag, t BookTags, opts WriteOptions) []u8 {
return out return out
} }
// Запись тэгов: новый тэг + аудио из исходного файла, через временный файл. // Запись тэгов: новый тэг + аудио из исходного файла собираются во временном файле,
// затем копируются обратно в исходный. Файл остаётся тем же (inode, владелец, права):
// Audiobookshelf опознаёт файлы по inode, и замена файла новым путает его библиотеку.
// Файл без ID3v2 тоже поддерживается — тэг просто ставится в начало // Файл без ID3v2 тоже поддерживается — тэг просто ставится в начало
fn write_book_tags_impl(path string, t BookTags, opts WriteOptions) ! { fn write_book_tags_impl(path string, t BookTags, opts WriteOptions) !WriteReport {
parsed := parse_id3v2(path)! parsed := parse_id3v2(path)!
tag := build_tag(parsed, t, opts) tag := build_tag(parsed, t, opts)
// Владелец и права исходного файла: перезапись через временный файл иначе их потеряет
// (на NAS файлы библиотеки принадлежат пользователю приложения, а агент может работать от root)
orig := os.stat(path) or { os.Stat{} }
tmp_path := path + '.tagtmp' tmp_path := path + '.tagtmp'
mut src := os.open(path)! mut buf := []u8{len: 1024 * 1024}
mut dst := os.create(tmp_path)! {
mut ok := false mut src := os.open(path)!
defer { defer {
src.close() src.close()
dst.close() }
if !ok { mut dst := os.create(tmp_path)!
defer {
dst.close()
}
dst.write(tag) or {
os.rm(tmp_path) or {} os.rm(tmp_path) or {}
return err
}
src.seek(i64(parsed.audio_start), .start)!
for {
n := src.read(mut buf) or { 0 }
if n <= 0 {
break
}
dst.write(buf[..n]) or {
os.rm(tmp_path) or {}
return err
}
} }
} }
copy_back(tmp_path, path, mut buf) or {
// Исходный файл мог остаться наполовину переписанным — полная копия во временном
reason := err.msg()
orig := os.stat(path) or { os.Stat{} }
os.mv(tmp_path, path, overwrite: true) or {
return error('${err}; новое содержимое сохранено в ${tmp_path}')
}
if orig.mode != 0 {
os.chmod(path, int(orig.mode & 0o7777)) or {}
$if !windows {
os.chown(path, int(orig.uid), int(orig.gid)) or {}
}
}
return WriteReport{
fallback: reason
}
}
os.rm(tmp_path) or {}
return WriteReport{
in_place: true
}
}
dst.write(tag)! // Содержимое from — в существующий файл to (с обрезкой), не заменяя сам файл
src.seek(i64(parsed.audio_start), .start)! fn copy_back(from string, to string, mut buf []u8) ! {
mut buf := []u8{len: 1024 * 1024} mut src := os.open(from)!
defer {
src.close()
}
mut dst := os.open_file(to, 'w')!
defer {
dst.close()
}
for { for {
n := src.read(mut buf) or { 0 } n := src.read(mut buf) or { 0 }
if n <= 0 { if n <= 0 {
@@ -589,16 +641,7 @@ fn write_book_tags_impl(path string, t BookTags, opts WriteOptions) ! {
} }
dst.write(buf[..n])! dst.write(buf[..n])!
} }
src.close() dst.flush()
dst.close()
ok = true
if orig.mode != 0 {
os.chmod(tmp_path, int(orig.mode & 0o7777)) or {}
$if !windows {
os.chown(tmp_path, int(orig.uid), int(orig.gid)) or {}
}
}
os.mv(tmp_path, path, overwrite: true)!
} }
// ---------- публичный API с защитой от паники ---------- // ---------- публичный API с защитой от паники ----------
@@ -635,8 +678,13 @@ pub fn read_cover(path string) !Cover {
// Запись тэгов книги в файл // Запись тэгов книги в файл
pub fn write_book_tags(path string, t BookTags, opts WriteOptions) ! { pub fn write_book_tags(path string, t BookTags, opts WriteOptions) ! {
write_book_tags_report(path, t, opts)!
}
// То же, что write_book_tags, плюс отчёт: переписан ли файл на месте
pub fn write_book_tags_report(path string, t BookTags, opts WriteOptions) !WriteReport {
mut box := PanicBox{} mut box := PanicBox{}
guard_write_tags(path, t, opts, mut box) or { report := guard_write_tags(path, t, opts, mut box) or {
return if box.msg != '' { return if box.msg != '' {
box.error() box.error()
} else { } else {
@@ -646,6 +694,7 @@ pub fn write_book_tags(path string, t BookTags, opts WriteOptions) ! {
if box.msg != '' { if box.msg != '' {
return box.error() return box.error()
} }
return report
} }
fn guard_read_tags(path string, mut box PanicBox) !FileTags { fn guard_read_tags(path string, mut box PanicBox) !FileTags {
@@ -666,11 +715,11 @@ fn guard_read_cover(path string, mut box PanicBox) !Cover {
return read_cover_impl(path)! return read_cover_impl(path)!
} }
fn guard_write_tags(path string, t BookTags, opts WriteOptions, mut box PanicBox) ! { fn guard_write_tags(path string, t BookTags, opts WriteOptions, mut box PanicBox) !WriteReport {
defer { defer {
if msg := recover() { if msg := recover() {
box.msg = msg box.msg = msg
} }
} }
write_book_tags_impl(path, t, opts)! return write_book_tags_impl(path, t, opts)!
} }
+20
View File
@@ -131,3 +131,23 @@ fn test_write_keeps_mode() {
assert after.mode & 0o777 == before.mode & 0o777 assert after.mode & 0o777 == before.mode & 0o777
assert after.uid == before.uid assert after.uid == before.uid
} }
// Файл остаётся тем же: Audiobookshelf опознаёт файлы по inode
fn test_write_keeps_inode() {
path := tmp_copy('test_id3v23.mp3')
defer {
os.rm(path) or {}
}
before := os.stat(path) or { panic(err) }
metadata.write_book_tags(path, metadata.BookTags{
title: 'Тот же файл'
comment: 'длинное описание '.repeat(500)
}) or { panic(err) }
after := os.stat(path) or { panic(err) }
$if !windows {
assert after.inode == before.inode
}
assert !os.exists(path + '.tagtmp')
t := metadata.read_book_tags(path) or { panic(err) }
assert t.tags.title == 'Тот же файл'
}