first version

This commit is contained in:
Q
2022-01-07 18:55:57 +02:00
commit 671823cbc6
7 changed files with 271 additions and 0 deletions

19
LICENSE.txt Normal file
View 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
View 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
View 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
View 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
View 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
View 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",
],
},
)

20
test.sh Normal file
View File

@@ -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