sachet/server/commands.py: added cleanup

This commit is contained in:
dogeystamp 2023-04-30 19:20:16 -04:00
parent 32618dec69
commit 4337a921ae
Signed by: dogeystamp
GPG Key ID: 7225FE3592EFFA38
5 changed files with 105 additions and 50 deletions

View File

@ -12,32 +12,31 @@ config = context.config
# Interpret the config file for Python logging. # Interpret the config file for Python logging.
# This line sets up loggers basically. # This line sets up loggers basically.
fileConfig(config.config_file_name) fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env') logger = logging.getLogger("alembic.env")
def get_engine(): def get_engine():
try: try:
# this works with Flask-SQLAlchemy<3 and Alchemical # this works with Flask-SQLAlchemy<3 and Alchemical
return current_app.extensions['migrate'].db.get_engine() return current_app.extensions["migrate"].db.get_engine()
except TypeError: except TypeError:
# this works with Flask-SQLAlchemy>=3 # this works with Flask-SQLAlchemy>=3
return current_app.extensions['migrate'].db.engine return current_app.extensions["migrate"].db.engine
def get_engine_url(): def get_engine_url():
try: try:
return get_engine().url.render_as_string(hide_password=False).replace( return get_engine().url.render_as_string(hide_password=False).replace("%", "%%")
'%', '%%')
except AttributeError: except AttributeError:
return str(get_engine().url).replace('%', '%%') return str(get_engine().url).replace("%", "%%")
# add your model's MetaData object here # add your model's MetaData object here
# for 'autogenerate' support # for 'autogenerate' support
# from myapp import mymodel # from myapp import mymodel
# target_metadata = mymodel.Base.metadata # target_metadata = mymodel.Base.metadata
config.set_main_option('sqlalchemy.url', get_engine_url()) config.set_main_option("sqlalchemy.url", get_engine_url())
target_db = current_app.extensions['migrate'].db target_db = current_app.extensions["migrate"].db
# other values from the config, defined by the needs of env.py, # other values from the config, defined by the needs of env.py,
# can be acquired: # can be acquired:
@ -46,7 +45,7 @@ target_db = current_app.extensions['migrate'].db
def get_metadata(): def get_metadata():
if hasattr(target_db, 'metadatas'): if hasattr(target_db, "metadatas"):
return target_db.metadatas[None] return target_db.metadatas[None]
return target_db.metadata return target_db.metadata
@ -64,9 +63,7 @@ def run_migrations_offline():
""" """
url = config.get_main_option("sqlalchemy.url") url = config.get_main_option("sqlalchemy.url")
context.configure( context.configure(url=url, target_metadata=get_metadata(), literal_binds=True)
url=url, target_metadata=get_metadata(), literal_binds=True
)
with context.begin_transaction(): with context.begin_transaction():
context.run_migrations() context.run_migrations()
@ -84,11 +81,11 @@ def run_migrations_online():
# when there are no changes to the schema # when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives): def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False): if getattr(config.cmd_opts, "autogenerate", False):
script = directives[0] script = directives[0]
if script.upgrade_ops.is_empty(): if script.upgrade_ops.is_empty():
directives[:] = [] directives[:] = []
logger.info('No changes in schema detected.') logger.info("No changes in schema detected.")
connectable = get_engine() connectable = get_engine()
@ -97,7 +94,7 @@ def run_migrations_online():
connection=connection, connection=connection,
target_metadata=get_metadata(), target_metadata=get_metadata(),
process_revision_directives=process_revision_directives, process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args **current_app.extensions["migrate"].configure_args
) )
with context.begin_transaction(): with context.begin_transaction():

View File

@ -11,7 +11,7 @@ import sqlalchemy_utils
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '4cd7cdbc2d1f' revision = "4cd7cdbc2d1f"
down_revision = None down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -19,43 +19,50 @@ depends_on = None
def upgrade(): def upgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('blacklist_tokens', op.create_table(
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), "blacklist_tokens",
sa.Column('token', sa.String(length=500), nullable=False), sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column('expires', sa.DateTime(), nullable=False), sa.Column("token", sa.String(length=500), nullable=False),
sa.PrimaryKeyConstraint('id'), sa.Column("expires", sa.DateTime(), nullable=False),
sa.UniqueConstraint('token') sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("token"),
) )
op.create_table('server_settings', op.create_table(
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), "server_settings",
sa.Column('default_permissions_number', sa.BigInteger(), nullable=False), sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.PrimaryKeyConstraint('id') sa.Column("default_permissions_number", sa.BigInteger(), nullable=False),
sa.PrimaryKeyConstraint("id"),
) )
op.create_table('users', op.create_table(
sa.Column('username', sa.String(length=255), nullable=False), "users",
sa.Column('password', sa.String(length=255), nullable=False), sa.Column("username", sa.String(length=255), nullable=False),
sa.Column('register_date', sa.DateTime(), nullable=False), sa.Column("password", sa.String(length=255), nullable=False),
sa.Column('permissions_number', sa.BigInteger(), nullable=False), sa.Column("register_date", sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('username'), sa.Column("permissions_number", sa.BigInteger(), nullable=False),
sa.UniqueConstraint('username') sa.PrimaryKeyConstraint("username"),
sa.UniqueConstraint("username"),
) )
op.create_table('shares', op.create_table(
sa.Column('share_id', sqlalchemy_utils.types.uuid.UUIDType(), nullable=False), "shares",
sa.Column('owner_name', sa.String(), nullable=True), sa.Column("share_id", sqlalchemy_utils.types.uuid.UUIDType(), nullable=False),
sa.Column('initialized', sa.Boolean(), nullable=False), sa.Column("owner_name", sa.String(), nullable=True),
sa.Column('locked', sa.Boolean(), nullable=False), sa.Column("initialized", sa.Boolean(), nullable=False),
sa.Column('create_date', sa.DateTime(), nullable=False), sa.Column("locked", sa.Boolean(), nullable=False),
sa.Column('file_name', sa.String(), nullable=False), sa.Column("create_date", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['owner_name'], ['users.username'], ), sa.Column("file_name", sa.String(), nullable=False),
sa.PrimaryKeyConstraint('share_id') sa.ForeignKeyConstraint(
["owner_name"],
["users.username"],
),
sa.PrimaryKeyConstraint("share_id"),
) )
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade(): def downgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.drop_table('shares') op.drop_table("shares")
op.drop_table('users') op.drop_table("users")
op.drop_table('server_settings') op.drop_table("server_settings")
op.drop_table('blacklist_tokens') op.drop_table("blacklist_tokens")
# ### end Alembic commands ### # ### end Alembic commands ###

View File

@ -1,9 +1,10 @@
import click import click
from sachet.server import app, db from sachet.server import app, db
from sachet.server.models import User, Permissions from sachet.server.models import User, Share, Permissions
from sachet.server.users import manage from sachet.server.users import manage
from flask.cli import AppGroup from flask.cli import AppGroup
from bitmask import Bitmask from bitmask import Bitmask
import datetime
user_cli = AppGroup("user") user_cli = AppGroup("user")
@ -44,3 +45,22 @@ def delete_user(username):
app.cli.add_command(user_cli) app.cli.add_command(user_cli)
@user_cli.command("cleanup")
def cleanup():
"""Clean up stale database entries.
Shares that are not initialized are deleted if they are older than 25 minutes.
"""
res = Share.query.filter(
Share.create_date < (datetime.datetime.now() - datetime.timedelta(minutes=25)),
# do not use `Share.initialized` or `is False` here
# sqlalchemy doesn't like it
Share.initialized == False,
)
res.delete()
db.session.commit()
app.cli.add_command(cleanup)

View File

@ -14,12 +14,14 @@ class BaseConfig:
class TestingConfig(BaseConfig): class TestingConfig(BaseConfig):
SERVER_NAME = "localhost.test"
SQLALCHEMY_DATABASE_URI = sqlalchemy_base + "_test" + ".db" SQLALCHEMY_DATABASE_URI = sqlalchemy_base + "_test" + ".db"
BCRYPT_LOG_ROUNDS = 4 BCRYPT_LOG_ROUNDS = 4
SACHET_FILE_DIR = "storage_test" SACHET_FILE_DIR = "storage_test"
class DevelopmentConfig(BaseConfig): class DevelopmentConfig(BaseConfig):
SERVER_NAME = "localhost.dev"
SQLALCHEMY_DATABASE_URI = sqlalchemy_base + "_dev" + ".db" SQLALCHEMY_DATABASE_URI = sqlalchemy_base + "_dev" + ".db"
BCRYPT_LOG_ROUNDS = 4 BCRYPT_LOG_ROUNDS = 4
SACHET_FILE_DIR = "storage_dev" SACHET_FILE_DIR = "storage_dev"

View File

@ -1,8 +1,9 @@
import pytest import pytest
from sachet.server.commands import create_user, delete_user from sachet.server.commands import create_user, delete_user, cleanup
from sqlalchemy import inspect from sqlalchemy import inspect
from sachet.server import db
from sachet.server.models import User import datetime
from sachet.server.models import User, Share
def test_user(client, cli): def test_user(client, cli):
@ -24,3 +25,31 @@ def test_user(client, cli):
# delete non-existent user # delete non-existent user
result = cli.invoke(delete_user, ["--yes", "jeff"]) result = cli.invoke(delete_user, ["--yes", "jeff"])
assert isinstance(result.exception, KeyError) assert isinstance(result.exception, KeyError)
def test_cleanup(client, cli):
"""Test the CLI's ability to destroy uninitialized shares past expiry."""
# create shares
# this one will be destroyed
share = Share()
db.session.add(share)
share.create_date = datetime.datetime.now() - datetime.timedelta(minutes=30)
destroyed = share.share_id
# this one won't
share = Share()
db.session.add(share)
safe = share.share_id
# this one neither
share = Share()
share.initialized = True
share.create_date = datetime.datetime.now() - datetime.timedelta(minutes=30)
db.session.add(share)
safe2 = share.share_id
db.session.commit()
result = cli.invoke(cleanup)
assert result.exit_code == 0
assert Share.query.filter_by(share_id=destroyed).first() is None
assert Share.query.filter_by(share_id=safe).first() is not None
assert Share.query.filter_by(share_id=safe2).first() is not None