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

View File

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

View File

@ -1,9 +1,10 @@
import click
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 flask.cli import AppGroup
from bitmask import Bitmask
import datetime
user_cli = AppGroup("user")
@ -44,3 +45,22 @@ def delete_user(username):
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):
SERVER_NAME = "localhost.test"
SQLALCHEMY_DATABASE_URI = sqlalchemy_base + "_test" + ".db"
BCRYPT_LOG_ROUNDS = 4
SACHET_FILE_DIR = "storage_test"
class DevelopmentConfig(BaseConfig):
SERVER_NAME = "localhost.dev"
SQLALCHEMY_DATABASE_URI = sqlalchemy_base + "_dev" + ".db"
BCRYPT_LOG_ROUNDS = 4
SACHET_FILE_DIR = "storage_dev"

View File

@ -1,8 +1,9 @@
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 sachet.server.models import User
from sachet.server import db
import datetime
from sachet.server.models import User, Share
def test_user(client, cli):
@ -24,3 +25,31 @@ def test_user(client, cli):
# delete non-existent user
result = cli.invoke(delete_user, ["--yes", "jeff"])
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