Skip to content

Commit 89eebaa

Browse files
committed
Release v1.0.0
0 parents  commit 89eebaa

File tree

8 files changed

+301
-0
lines changed

8 files changed

+301
-0
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
*.egg-info/
2+
.idea
3+
__pycache__
4+
build/
5+
dist/
6+
venv

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2022 Halit Şimşek
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Mtime Fixer
2+
Mtime Fixer is a tool for fixing inconsistent timestamp metadata (atime, ctime, and mtime).
3+
Sometimes timestamp metadata of folders are inconsistent with files inside them.
4+
Mtime Fixer is a program for setting mtime (modification time) timestamp of folders according to files inside them.
5+
Additionally, it can set ctime (change time) metadata as the same as mtime as well (with root privileges).
6+
7+
## Requirements
8+
Python >= 3.8 is required. (CPython and PyPy are both supported)
9+
10+
## Installation
11+
Mtime Fixer can be either installed directly via pip:
12+
```shell
13+
pip install mtime-fixer
14+
```
15+
Or it can be installed from the source:
16+
```shell
17+
git clone https://github.com/simsekhalit/mtime-fixer.git
18+
python3 -m pip install ./mtime-fixer
19+
```
20+
21+
## Manual
22+
```
23+
$ python3 -m mtime_fixer --help
24+
usage: mtime-fixer [-h] [-c] [-f] PATH [PATH ...]
25+
26+
A tool for fixing inconsistent timestamp metadata (atime, ctime, and mtime).
27+
28+
positional arguments:
29+
PATH
30+
31+
optional arguments:
32+
-h, --help show this help message and exit
33+
-c, --fix-ctimes change ctimes as well (requires root priviledges)
34+
-f, --fix-files change timestamps of files as well
35+
36+
For more information: https://github.com/simsekhalit/mtime-fixer
37+
```

mtime_fixer/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env python3
2+
from .core import *
3+
4+
__version__ = "1.0.0"
5+
__version_info__ = (1, 0, 0)

mtime_fixer/__main__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env python3
2+
import sys
3+
4+
from .cli import main
5+
6+
main(sys.argv[1:])

mtime_fixer/cli.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env python3
2+
import argparse
3+
import sys
4+
from typing import Sequence
5+
6+
from .core import MtimeFixer
7+
8+
__all__ = [
9+
"main",
10+
"parse_args",
11+
]
12+
13+
14+
def parse_args(args: Sequence[str]) -> argparse.Namespace:
15+
description = "A tool for fixing inconsistent timestamp metadata (atime, ctime, and mtime)."
16+
epilog = "For more information: https://github.com/simsekhalit/mtime-fixer"
17+
parser = argparse.ArgumentParser("mtime-fixer", description=description, epilog=epilog)
18+
parser.add_argument("-c", "--fix-ctimes", action="store_true",
19+
help="change ctimes as well (requires root priviledges)")
20+
parser.add_argument("-f", "--fix-files", action="store_true", help="change timestamps of files as well")
21+
parser.add_argument("paths", metavar="PATH", nargs="+")
22+
23+
return parser.parse_args(args)
24+
25+
26+
def main(args: Sequence[str]) -> None:
27+
args = parse_args(args)
28+
mtime_fixer = MtimeFixer(args.fix_ctimes, args.fix_files)
29+
mtime_fixer.fix(args.paths)
30+
31+
32+
if __name__ == "__main__":
33+
main(sys.argv[1:])

mtime_fixer/core.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import stat
4+
from typing import List
5+
6+
7+
class MtimeFixer:
8+
def __init__(self, fix_ctimes: bool = False, fix_files: bool = False):
9+
self.fix_ctimes: bool = False
10+
self.fix_files: bool = fix_files
11+
self.librt = None
12+
13+
if fix_ctimes:
14+
if self._setup_librt():
15+
self.fix_ctimes = True
16+
17+
def _setup_librt(self) -> bool:
18+
global ctypes, timespec
19+
import ctypes.util
20+
21+
librt = ctypes.util.find_library("rt")
22+
if not librt:
23+
return False
24+
try:
25+
self.librt = ctypes.CDLL(librt)
26+
except Exception:
27+
return False
28+
29+
class timespec(ctypes.Structure):
30+
_fields_ = [("tv_sec", ctypes.c_long),
31+
("tv_nsec", ctypes.c_long)]
32+
33+
return True
34+
35+
@staticmethod
36+
def _enable_ntp() -> None:
37+
import subprocess
38+
popen = subprocess.Popen(["timedatectl", "set-ntp", "true"], stdin=subprocess.DEVNULL,
39+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)
40+
try:
41+
popen.wait(10)
42+
except subprocess.TimeoutExpired:
43+
pass
44+
45+
def _set_time(self, time_ns: int) -> None:
46+
ts = timespec(time_ns // 1_000_000_000, time_ns % 1_000_000_000)
47+
self.librt.clock_settime(0, ctypes.byref(ts))
48+
49+
def _fix_times(self, path: str, time_ns: int) -> None:
50+
if self.fix_ctimes:
51+
self._set_time(time_ns)
52+
os.utime(path, ns=(time_ns, time_ns), follow_symlinks=False)
53+
54+
def _is_fix_needed(self, stat_: os.stat_result, time_ns: int) -> bool:
55+
if not self.fix_files and not stat.S_ISDIR(stat_.st_mode):
56+
return False
57+
elif self.fix_ctimes and abs(stat_.st_ctime_ns - time_ns) > 1_000_000_000:
58+
return True
59+
elif abs(stat_.st_mtime_ns - time_ns) > 1_000_000_000:
60+
return True
61+
else:
62+
return False
63+
64+
def _fix_path(self, path: str) -> int:
65+
current_stat = os.stat(path, follow_symlinks=False)
66+
max_mtime = 0
67+
68+
if stat.S_ISDIR(current_stat.st_mode):
69+
for child in os.listdir(path):
70+
child_mtime = self._fix_path(os.path.join(path, child))
71+
if child_mtime > max_mtime:
72+
max_mtime = child_mtime
73+
74+
if max_mtime == 0:
75+
max_mtime = current_stat.st_mtime_ns
76+
77+
if self._is_fix_needed(current_stat, max_mtime):
78+
self._fix_times(path, max_mtime)
79+
return max_mtime
80+
81+
@staticmethod
82+
def _disable_ntp() -> bool:
83+
import subprocess
84+
try:
85+
subprocess.run(["timedatectl", "set-ntp", "false"], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE,
86+
stderr=subprocess.STDOUT, universal_newlines=True, timeout=10)
87+
except subprocess.TimeoutExpired as e:
88+
print(f"Failed to disable ntp:\n{e.output}")
89+
return False
90+
else:
91+
return True
92+
93+
def fix(self, paths: List[str]) -> bool:
94+
if self.fix_ctimes:
95+
success = self._disable_ntp()
96+
if not success:
97+
return False
98+
try:
99+
for path in paths:
100+
self._fix_path(path)
101+
finally:
102+
self._enable_ntp()
103+
else:
104+
for path in paths:
105+
self._fix_path(path)
106+
107+
return True

setup.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import re
4+
import shutil
5+
import subprocess
6+
import sys
7+
8+
from setuptools import Command, setup
9+
10+
11+
class UploadCommand(Command):
12+
"""Support setup.py upload."""
13+
14+
description = "Build and publish the package."
15+
user_options = []
16+
17+
@staticmethod
18+
def status(s):
19+
"""Prints things in bold."""
20+
print('\033[1m{0}\033[0m'.format(s))
21+
22+
def initialize_options(self):
23+
pass
24+
25+
def finalize_options(self):
26+
pass
27+
28+
def run(self):
29+
try:
30+
self.status("Cleaning previous builds...")
31+
for p in ("build", "dist", *(p for p in os.listdir(setup_dir) if p.endswith(".egg-info"))):
32+
shutil.rmtree(os.path.join(setup_dir, p), True)
33+
except OSError:
34+
pass
35+
36+
self.status("Building Source and Wheel (universal) distribution...")
37+
subprocess.run([sys.executable, "-m", "build"], check=True)
38+
39+
self.status("Uploading the package to PyPI via Twine...")
40+
dist_dir = os.path.join(setup_dir, "dist")
41+
subprocess.run(["twine", "upload", *(os.path.join(dist_dir, p) for p in os.listdir(dist_dir))], check=True)
42+
43+
self.status("Pushing git tags...")
44+
subprocess.run(["git", "tag", f"v{version}"], check=True)
45+
subprocess.run(["git", "push", "--tags"], check=True)
46+
47+
48+
setup_dir = os.path.abspath(os.path.dirname(__file__))
49+
50+
with open(os.path.join(setup_dir, "README.md"), "r") as f:
51+
long_description = f.read()
52+
53+
with open(os.path.join(setup_dir, "mtime_fixer", "__init__.py"), "r") as f:
54+
version = re.search(r"__version__\s*=\s*['\"](.+)['\"]", f.read()).group(1)
55+
56+
57+
setup(
58+
author="Halit Şimşek",
59+
author_email="mail.simsekhalit@gmail.com",
60+
classifiers=[
61+
"Development Status :: 5 - Production/Stable",
62+
"License :: OSI Approved :: MIT License",
63+
"Programming Language :: Python",
64+
"Programming Language :: Python :: 3",
65+
"Programming Language :: Python :: 3.8",
66+
"Programming Language :: Python :: 3.9",
67+
"Programming Language :: Python :: 3.10",
68+
"Programming Language :: Python :: 3.11",
69+
"Programming Language :: Python :: Implementation :: CPython",
70+
"Programming Language :: Python :: Implementation :: PyPy",
71+
"Topic :: System :: Archiving",
72+
],
73+
cmdclass={
74+
"upload": UploadCommand,
75+
},
76+
description="A tool for fixing inconsistent timestamp metadata (atime, ctime, and mtime).",
77+
keywords=["atime", "ctime", "metadata", "mtime"],
78+
license="MIT",
79+
long_description=long_description,
80+
long_description_content_type="text/markdown",
81+
name="mtime-fixer",
82+
packages=["mtime_fixer"],
83+
python_requires=">=3.8",
84+
url="https://www.github.com/simsekhalit/mtime-fixer",
85+
version=version,
86+
)

0 commit comments

Comments
 (0)