first version
This commit is contained in:
19
LICENSE.txt
Normal file
19
LICENSE.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2022 Ville Rantanen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
11
README.md
Normal file
11
README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# AtDel
|
||||
|
||||
A tool to delete files automatically after waiting period. (using cron)
|
||||
|
||||
## Installation
|
||||
|
||||
or `pipx install git+https://github.org/moonq/atdel.git`
|
||||
|
||||
## Usage
|
||||
|
||||
See command line help
|
||||
11
atdel/__init__.py
Normal file
11
atdel/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
__version__ = "20220107.1"
|
||||
|
||||
|
||||
def get_version():
|
||||
return __version__
|
||||
|
||||
|
||||
def main():
|
||||
from atdel.atdel import AtDel
|
||||
|
||||
AtDel()
|
||||
188
atdel/atdel.py
Executable file
188
atdel/atdel.py
Executable file
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
from datetime import datetime, timedelta
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
|
||||
class AtDel:
|
||||
def __init__(self):
|
||||
self.config_file = os.path.expanduser("~/.cache/atdel")
|
||||
self.parse_opts()
|
||||
self.db_init()
|
||||
|
||||
if self.options.days is not None:
|
||||
self.set_file_status()
|
||||
|
||||
if self.options.days is None and not self.options.delete:
|
||||
self.db_list()
|
||||
|
||||
if self.options.delete:
|
||||
self.del_due_files()
|
||||
|
||||
def db_init(self):
|
||||
|
||||
with sqlite3.connect(self.config_file) as con:
|
||||
con.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS atdel (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
inode INTEGER NOT NULL,
|
||||
added TEXT NOT NULL,
|
||||
due TEXT NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
con.execute(
|
||||
"""
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_name ON atdel (name);
|
||||
"""
|
||||
)
|
||||
|
||||
def parse_opts(self):
|
||||
"""Options parser
|
||||
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Automatically delete files after due date. Note: file must have same inode to be deleted",
|
||||
epilog="Automate deletion by adding cron '0 0 * * * atdel --delete'"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verbose",
|
||||
"-v",
|
||||
action="store_true",
|
||||
dest="verbose",
|
||||
default=False,
|
||||
help="Increase verbosity",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--delete",
|
||||
action="store_true",
|
||||
dest="delete",
|
||||
default=False,
|
||||
help="Delete all due files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
action="store",
|
||||
type=int,
|
||||
help="Days to keep files. 0 to remove deletion tag",
|
||||
default=None,
|
||||
dest="days",
|
||||
)
|
||||
parser.add_argument(
|
||||
"files",
|
||||
action="store",
|
||||
type=str,
|
||||
help="Files/folders to delete after N days",
|
||||
default=[],
|
||||
nargs="*",
|
||||
)
|
||||
self.options = parser.parse_args()
|
||||
if self.options.days is None and len(self.options.files) > 0:
|
||||
parser.error("If files set, must give -d for retain length")
|
||||
|
||||
def set_file_status(self):
|
||||
|
||||
to_remove = self.options.days == 0
|
||||
now = datetime.now()
|
||||
del_delta = timedelta(days=self.options.days)
|
||||
del_time = (now + del_delta).isoformat()
|
||||
now_time = now.isoformat()
|
||||
|
||||
with sqlite3.connect(self.config_file) as con:
|
||||
for f in self.options.files:
|
||||
path = os.path.abspath(f)
|
||||
inode = os.stat(f).st_ino
|
||||
|
||||
if to_remove:
|
||||
rows = con.execute("SELECT name FROM atdel WHERE name = ?", (path,))
|
||||
if len(rows.fetchall()) == 0:
|
||||
print("No such file in database: '{}'".format(path))
|
||||
else:
|
||||
con.execute(
|
||||
"DELETE FROM atdel WHERE name = ?;",
|
||||
(path,),
|
||||
)
|
||||
print("Removed: {}".format(path))
|
||||
else:
|
||||
con.execute(
|
||||
"INSERT OR REPLACE INTO atdel (name, due, added, inode) values(?, ?, ?, ?);",
|
||||
(path, del_time, now_time, inode),
|
||||
)
|
||||
print(f)
|
||||
if not to_remove:
|
||||
print(
|
||||
"To be deleted in {} days, or on {}".format(self.options.days, del_time)
|
||||
)
|
||||
|
||||
def db_list(self):
|
||||
|
||||
|
||||
data = []
|
||||
with sqlite3.connect(self.config_file) as con:
|
||||
rows = con.execute("SELECT added, due, name FROM atdel ORDER BY added;")
|
||||
for row in rows:
|
||||
due = (datetime.fromisoformat(row[1]) - datetime.now()).days
|
||||
rel = os.path.relpath(row[2])
|
||||
if rel.startswith(".."):
|
||||
rel = row[2]
|
||||
data.append([row[0][0:10], row[1][0:10], due, rel])
|
||||
print("{:10s} {:10s} {:4s} {}".format("Added", "Due", "Days", "File"))
|
||||
for row in data:
|
||||
print("{:10s} {:10s} {:4d} {}".format(*row))
|
||||
|
||||
def del_due_files(self):
|
||||
"""Delete files where due date has passed"""
|
||||
|
||||
paths = []
|
||||
with sqlite3.connect(self.config_file) as con:
|
||||
rows = con.execute(
|
||||
"SELECT added, due, name, inode FROM atdel ORDER BY added;"
|
||||
)
|
||||
for row in rows:
|
||||
due = (datetime.fromisoformat(row[1]) - datetime.now()).days
|
||||
exists = os.path.exists(row[2])
|
||||
if due < 0 or not exists:
|
||||
paths.append([row[2], row[3]])
|
||||
|
||||
for (p, inode) in paths:
|
||||
try:
|
||||
if not os.path.exists(p):
|
||||
print("File {} doesnt exist, removing from DB".format(p))
|
||||
else:
|
||||
curr_inode = os.stat(p).st_ino
|
||||
if curr_inode != inode:
|
||||
print(
|
||||
"Path has different inode, possible security issue: {}".format(
|
||||
p
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
continue
|
||||
if os.path.isdir(p):
|
||||
print("Deleting folder {}".format(p))
|
||||
shutil.rmtree(p)
|
||||
else:
|
||||
print("Deleting file {}".format(p))
|
||||
os.remove(p)
|
||||
|
||||
# ~ self.db_remove(p)
|
||||
except Exception as e:
|
||||
print(e, file=sys.stderr)
|
||||
|
||||
def db_remove(self, path):
|
||||
with sqlite3.connect(self.config_file) as con:
|
||||
con.execute(
|
||||
"""
|
||||
DELETE FROM atdel WHERE name = ?;
|
||||
""",
|
||||
(path,),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
atdel = AtDel()
|
||||
9
setup.cfg
Normal file
9
setup.cfg
Normal file
@@ -0,0 +1,9 @@
|
||||
[metadata]
|
||||
description-file = README.md
|
||||
version = attr: atdel.__version__
|
||||
license = MIT
|
||||
description = Delete files after X days.
|
||||
author = Ville Rantanen
|
||||
author_email = ville.q.rantanen@gmail.com
|
||||
url = https://bitbucket.org/MoonQ/atdel
|
||||
download_url = https://bitbucket.org/MoonQ/atdel/get/master.zip
|
||||
13
setup.py
Normal file
13
setup.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from distutils.core import setup
|
||||
|
||||
setup(
|
||||
name="atdel",
|
||||
packages=["atdel"],
|
||||
include_package_data=True,
|
||||
classifiers=[],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"atdel=atdel:main",
|
||||
],
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user