Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option to preserve file mtime on save #645

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions mutagen/_iff.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
delete_bytes,
insert_bytes,
loadfile,
set_restore_mtime,
reraise,
resize_bytes,
)
Expand Down Expand Up @@ -364,11 +365,14 @@ def _pre_load_header(self, fileobj):

@convert_error(IOError, error)
@loadfile(writable=True)
def save(self, filething=None, v2_version=4, v23_sep='/', padding=None):
def save(self, filething=None, v2_version=4, v23_sep='/',
padding=None, preserve_mtime=False):
"""Save ID3v2 data to the IFF file"""

fileobj = filething.fileobj

if preserve_mtime:
set_restore_mtime(fileobj)

iff_file = self._load_file(fileobj)

if 'ID3' not in iff_file:
Expand Down
38 changes: 27 additions & 11 deletions mutagen/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import codecs
import errno
import decimal
import os
import time
from io import BytesIO
from typing import Tuple, List

Expand Down Expand Up @@ -271,21 +273,35 @@ def _openfile(instance, filething, filename, fileobj, writable, create):
else:
raise MutagenError(e)

with fileobj as fileobj:
yield FileThing(fileobj, filename, filename)

if inmemory_fileobj:
assert writable
data = fileobj.getvalue()
try:
with open(filename, "wb") as fileobj:
fileobj.write(data)
except IOError as e:
raise MutagenError(e)
try:
with fileobj as fileobj:
yield FileThing(fileobj, filename, filename)

if inmemory_fileobj:
assert writable
data = fileobj.getvalue()
try:
with open(filename, "wb") as fileobj:
fileobj.write(data)
except IOError as e:
raise MutagenError(e)
finally:
if hasattr(fileobj, "__restore_mtime__"):
new_atime = time.time_ns()
original_mtime = fileobj.__restore_mtime__
print("\nRetaining original mtime. file={}, atime={}, o_mtime={}"
.format(filename, new_atime, original_mtime))
os.utime(filename, ns=(new_atime, original_mtime))
else:
raise TypeError("Missing filename or fileobj argument")


def set_restore_mtime(fileobj):
if fileobj is not None:
original_mtime = os.stat(fileobj.name).st_mtime_ns
setattr(fileobj, "__restore_mtime__", original_mtime)


class MutagenError(Exception):
"""Base class for all custom exceptions in mutagen

Expand Down
8 changes: 6 additions & 2 deletions mutagen/apev2.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@

from mutagen import Metadata, FileType, StreamInfo
from mutagen._util import DictMixin, cdata, delete_bytes, total_ordering, \
MutagenError, loadfile, convert_error, seek_end, get_size, reraise
MutagenError, loadfile, convert_error, seek_end, get_size, reraise, \
set_restore_mtime


def is_valid_apev2_key(key):
Expand Down Expand Up @@ -396,7 +397,7 @@ def __setitem__(self, key, value):

@convert_error(IOError, error)
@loadfile(writable=True, create=True)
def save(self, filething=None):
def save(self, filething=None, preserve_mtime=False):
"""Save changes to a file.

If no filename is given, the one most recently loaded is used.
Expand All @@ -407,6 +408,9 @@ def save(self, filething=None):

fileobj = filething.fileobj

if preserve_mtime:
set_restore_mtime(fileobj)

data = _APEv2Data(fileobj)

if data.is_at_start:
Expand Down
8 changes: 6 additions & 2 deletions mutagen/asf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
__all__ = ["ASF", "Open"]

from mutagen import FileType, Tags, StreamInfo
from mutagen._util import resize_bytes, DictMixin, loadfile, convert_error
from mutagen._util import resize_bytes, DictMixin, loadfile, convert_error, \
set_restore_mtime

from ._util import error, ASFError, ASFHeaderError
from ._objects import HeaderObject, MetadataLibraryObject, MetadataObject, \
Expand Down Expand Up @@ -245,7 +246,7 @@ def load(self, filething):

@convert_error(IOError, error)
@loadfile(writable=True)
def save(self, filething=None, padding=None):
def save(self, filething=None, padding=None, preserve_mtime=False):
"""save(filething=None, padding=None)

Save tag changes back to the loaded file.
Expand Down Expand Up @@ -300,6 +301,9 @@ def save(self, filething=None, padding=None):
header_ext.objects.append(MetadataLibraryObject())

fileobj = filething.fileobj
if preserve_mtime:
set_restore_mtime(fileobj)

# Render to file
old_size = header.parse_size(fileobj)[0]
data = header.render_full(self, fileobj, old_size, padding)
Expand Down
9 changes: 7 additions & 2 deletions mutagen/dsf.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from mutagen import FileType, StreamInfo
from mutagen._util import cdata, MutagenError, loadfile, \
convert_error, reraise, endswith
convert_error, reraise, endswith, set_restore_mtime
from mutagen.id3 import ID3
from mutagen.id3._util import ID3NoHeaderError, error as ID3Error

Expand Down Expand Up @@ -198,10 +198,15 @@ def _pre_load_header(self, fileobj):

@convert_error(IOError, error)
@loadfile(writable=True)
def save(self, filething=None, v2_version=4, v23_sep='/', padding=None):
def save(self, filething=None, v2_version=4, v23_sep='/', padding=None,
preserve_mtime=False):
"""Save ID3v2 data to the DSF file"""

fileobj = filething.fileobj

if preserve_mtime:
set_restore_mtime(fileobj)

fileobj.seek(0)

dsd_header = DSDChunk(fileobj)
Expand Down
10 changes: 6 additions & 4 deletions mutagen/easyid3.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,9 @@ def __init__(self, filename=None):

@loadfile(writable=True, create=True)
def save(self, filething=None, v1=1, v2_version=4, v23_sep='/',
padding=None):
"""save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None)
padding=None, preserve_mtime=False):
"""save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None,
preserve_mtime=False)

Save changes to a file.
See :meth:`mutagen.id3.ID3.save` for more info.
Expand All @@ -191,12 +192,13 @@ def save(self, filething=None, v1=1, v2_version=4, v23_sep='/',
self.__id3.update_to_v23()
self.__id3.save(
filething, v1=v1, v2_version=v2_version, v23_sep=v23_sep,
padding=padding)
padding=padding, preserve_mtime=preserve_mtime)
finally:
self.__id3._restore(backup)
else:
self.__id3.save(filething, v1=v1, v2_version=v2_version,
v23_sep=v23_sep, padding=padding)
v23_sep=v23_sep, padding=padding,
preserve_mtime=preserve_mtime)

delete = property(lambda s: s.__id3.delete,
lambda s, v: setattr(s.__id3, 'delete', v))
Expand Down
8 changes: 6 additions & 2 deletions mutagen/flac.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import mutagen

from mutagen._util import resize_bytes, MutagenError, get_size, loadfile, \
convert_error, bchr, endswith
convert_error, bchr, endswith, set_restore_mtime
from mutagen._tags import PaddingInfo
from mutagen.id3._util import BitPaddedInt
from functools import reduce
Expand Down Expand Up @@ -836,13 +836,14 @@ def pictures(self):

@convert_error(IOError, error)
@loadfile(writable=True)
def save(self, filething=None, deleteid3=False, padding=None):
def save(self, filething=None, deleteid3=False, padding=None, preserve_mtime=False):
"""Save metadata blocks to a file.

Args:
filething (filething)
deleteid3 (bool): delete id3 tags while at it
padding (:obj:`mutagen.PaddingFunction`)
preserve_mtime (bool): Keep existing modified time on save

If no filename is given, the one most recently loaded is used.
"""
Expand All @@ -856,6 +857,9 @@ def save(self, filething=None, deleteid3=False, padding=None):
raise ValueError("Invalid seektable object type!")
self.metadata_blocks.append(self.seektable)

if preserve_mtime:
set_restore_mtime(filething.fileobj)

self._save(filething, self.metadata_blocks, deleteid3, padding)

def _save(self, filething, metadata_blocks, deleteid3, padding):
Expand Down
12 changes: 9 additions & 3 deletions mutagen/id3/_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import mutagen
from mutagen._util import insert_bytes, delete_bytes, enum, \
loadfile, convert_error, read_full
loadfile, convert_error, read_full, set_restore_mtime
from mutagen._tags import PaddingInfo

from ._util import error, ID3NoHeaderError, ID3UnsupportedVersionError, \
Expand Down Expand Up @@ -221,8 +221,9 @@ def _prepare_data(self, fileobj, start, available, v2_version, v23_sep,
@convert_error(IOError, error)
@loadfile(writable=True, create=True)
def save(self, filething=None, v1=1, v2_version=4, v23_sep='/',
padding=None):
"""save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None)
padding=None, preserve_mtime=False):
"""save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None,
preserve_mtime=False)

Save changes to a file.

Expand All @@ -241,6 +242,8 @@ def save(self, filething=None, v1=1, v2_version=4, v23_sep='/',
if v2_version == 3. Defaults to '/' but if it's None
will be the ID3v2v2.4 null separator.
padding (:obj:`mutagen.PaddingFunction`)
preserve_mtime:
Keep the original file modified time as it was before saving.

Raises:
mutagen.MutagenError
Expand All @@ -253,6 +256,9 @@ def save(self, filething=None, v1=1, v2_version=4, v23_sep='/',

f = filething.fileobj

if preserve_mtime:
set_restore_mtime(f)

try:
header = ID3Header(filething.fileobj)
except ID3NoHeaderError:
Expand Down
9 changes: 6 additions & 3 deletions mutagen/mp4/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from mutagen._constants import GENRES
from mutagen._util import cdata, insert_bytes, DictProxy, MutagenError, \
hashable, enum, get_size, resize_bytes, loadfile, convert_error, bchr, \
reraise
reraise, set_restore_mtime
from ._atom import Atoms, Atom, AtomError
from ._util import parse_full_atom
from ._as_entry import AudioSampleEntry, ASEntryError
Expand Down Expand Up @@ -389,7 +389,7 @@ def _render(self, key, value):

@convert_error(IOError, error)
@loadfile(writable=True)
def save(self, filething=None, padding=None):
def save(self, filething=None, padding=None, preserve_mtime=False):

values = []
items = sorted(self.items(), key=lambda kv: _item_sort_key(*kv))
Expand Down Expand Up @@ -418,7 +418,10 @@ def save(self, filething=None, padding=None):
except AtomError as err:
reraise(error, err, sys.exc_info()[2])

self.__save(filething.fileobj, atoms, data, padding)
fileobj = filething.fileobj
if preserve_mtime:
set_restore_mtime(fileobj)
self.__save(fileobj, atoms, data, padding)

def __save(self, fileobj, atoms, data, padding):
try:
Expand Down
10 changes: 7 additions & 3 deletions mutagen/ogg.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

from mutagen import FileType
from mutagen._util import cdata, resize_bytes, MutagenError, loadfile, \
seek_end, bchr, reraise
seek_end, bchr, reraise, set_restore_mtime
from mutagen._file import StreamInfo
from mutagen._tags import Tags

Expand Down Expand Up @@ -571,8 +571,8 @@ def add_tags(self):
raise self._Error

@loadfile(writable=True)
def save(self, filething=None, padding=None):
"""save(filething=None, padding=None)
def save(self, filething=None, padding=None, preserve_mtime=False):
"""save(filething=None, padding=None. preserve_mtime=False))

Save a tag to a file.

Expand All @@ -581,11 +581,15 @@ def save(self, filething=None, padding=None):
Args:
filething (filething)
padding (:obj:`mutagen.PaddingFunction`)
preserve_mtime (bool)
Raises:
mutagen.MutagenError
"""

try:
if preserve_mtime:
set_restore_mtime(filething.fileobj)

self.tags._inject(filething.fileobj, padding)
except (IOError, error) as e:
reraise(self._Error, e, sys.exc_info()[2])
Expand Down
8 changes: 7 additions & 1 deletion mutagen/wave.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
endswith,
loadfile,
reraise,
set_restore_mtime
)

__all__ = ["WAVE", "Open", "delete"]
Expand Down Expand Up @@ -118,10 +119,15 @@ def _pre_load_header(self, fileobj):

@convert_error(IOError, error)
@loadfile(writable=True)
def save(self, filething, v1=1, v2_version=4, v23_sep='/', padding=None):
def save(self, filething, v1=1, v2_version=4, v23_sep='/', padding=None,
preserve_mtime=False):
"""Save ID3v2 data to the Wave/RIFF file"""

fileobj = filething.fileobj

if preserve_mtime:
set_restore_mtime(fileobj)

wave_file = _WaveFile(fileobj)

if u'id3' not in wave_file:
Expand Down
10 changes: 10 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ def get_temp_copy(path):
return filename


def get_temp_copy_keep_metadata(path):
"""Returns a copy of the file with the same extension"""

ext = os.path.splitext(path)[-1]
fd, filename = mkstemp(suffix=ext)
os.close(fd)
shutil.copy2(path, filename)
return filename


def get_temp_empty(ext=""):
"""Returns an empty file with the extension"""

Expand Down
Binary file added tests/data/silence-44-s-aged-filetime.mp3
Binary file not shown.
Loading
Loading