commit 671823cbc60b49bbabd0986d3a9ffc4633bd438f Author: Q Date: Fri Jan 7 18:55:57 2022 +0200 first version diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..92dbe08 --- /dev/null +++ b/LICENSE.txt @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a617a22 --- /dev/null +++ b/README.md @@ -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 diff --git a/atdel/__init__.py b/atdel/__init__.py new file mode 100644 index 0000000..86bee31 --- /dev/null +++ b/atdel/__init__.py @@ -0,0 +1,11 @@ +__version__ = "20220107.1" + + +def get_version(): + return __version__ + + +def main(): + from atdel.atdel import AtDel + + AtDel() diff --git a/atdel/atdel.py b/atdel/atdel.py new file mode 100755 index 0000000..05d8f83 --- /dev/null +++ b/atdel/atdel.py @@ -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() diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..fb86584 --- /dev/null +++ b/setup.cfg @@ -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 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..84c4ec5 --- /dev/null +++ b/setup.py @@ -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", + ], + }, +) diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..7c548aa --- /dev/null +++ b/test.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -x +touch foo +stat foo +atdel -d -2 foo + +rm foo + +touch bar foo fuu +mv fuu foo +rm bar +stat foo +atdel +atdel --delete + +atdel -d -2 foo +atdel --delete + +atdel -d -2 nonexist