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