diff --git a/default_config.py b/default_config.py index e3b0ac4..d7022bd 100644 --- a/default_config.py +++ b/default_config.py @@ -12,4 +12,7 @@ PORT="5000" MAX_ONLINE_QUERIES=3 # Disable registration of new users via /auth/register (they can still be added by root) -DISABLE_REGISTER=False \ No newline at end of file +DISABLE_REGISTER=False + +# Front URL of the application (for QRCodes generation) +BASE_URL="http://localhost:5000" \ No newline at end of file diff --git a/partitioncloud/modules/albums.py b/partitioncloud/modules/albums.py index 509e93b..aa5500f 100644 --- a/partitioncloud/modules/albums.py +++ b/partitioncloud/modules/albums.py @@ -4,14 +4,13 @@ Albums module """ import os import shutil -from uuid import uuid4 from flask import (Blueprint, abort, flash, redirect, render_template, request, send_file, session, current_app) from .auth import login_required from .db import get_db -from .utils import User, Album, get_all_partitions +from .utils import User, Album, get_all_partitions, new_uuid, get_qrcode, format_uuid from . import search bp = Blueprint("albums", __name__, url_prefix="/albums") @@ -71,25 +70,34 @@ def album(uuid): """ try: album = Album(uuid=uuid) - album.users = [User(user_id=i["id"]) for i in album.get_users()] - user = User(user_id=session.get("user_id")) - partitions = album.get_partitions() - if user.id is None: - # On ne propose pas aux gens non connectés de rejoindre l'album - not_participant = False - else: - not_participant = not user.is_participant(album.uuid) - - return render_template( - "albums/album.html", - album=album, - partitions=partitions, - not_participant=not_participant, - user=user - ) - except LookupError: - return abort(404) + try: + album = Album(uuid=format_uuid(uuid)) + return redirect(f"/albums/{format_uuid(uuid)}") + except LookupError: + return abort(404) + + album.users = [User(user_id=i["id"]) for i in album.get_users()] + user = User(user_id=session.get("user_id")) + partitions = album.get_partitions() + if user.id is None: + # On ne propose pas aux gens non connectés de rejoindre l'album + not_participant = False + else: + not_participant = not user.is_participant(album.uuid) + + return render_template( + "albums/album.html", + album=album, + partitions=partitions, + not_participant=not_participant, + user=user + ) + + +@bp.route("//qr") +def qr_code(uuid): + return get_qrcode(f"/albums/{uuid}") @bp.route("/create-album", methods=["POST"]) @@ -107,7 +115,7 @@ def create_album(): if error is None: while True: try: - uuid = str(uuid4()) + uuid = new_uuid() db.execute( """ diff --git a/partitioncloud/modules/groupe.py b/partitioncloud/modules/groupe.py index 84963d8..f98d91f 100644 --- a/partitioncloud/modules/groupe.py +++ b/partitioncloud/modules/groupe.py @@ -3,14 +3,13 @@ Groupe module """ import os -from uuid import uuid4 from flask import (Blueprint, abort, flash, redirect, render_template, request, send_file, session, current_app) from .auth import login_required from .db import get_db -from .utils import User, Album, get_all_partitions, Groupe +from .utils import User, Album, get_all_partitions, Groupe, new_uuid, get_qrcode, format_uuid from . import search bp = Blueprint("groupe", __name__, url_prefix="/groupe") @@ -28,25 +27,34 @@ def groupe(uuid): """ try: groupe = Groupe(uuid=uuid) - groupe.users = [User(user_id=i["id"]) for i in groupe.get_users()] - groupe.get_albums() - user = User(user_id=session.get("user_id")) - - if user.id is None: - # On ne propose pas aux gens non connectés de rejoindre l'album - not_participant = False - else: - not_participant = not user.id in [i.id for i in groupe.users] - - return render_template( - "groupe/index.html", - groupe=groupe, - not_participant=not_participant, - user=user - ) - except LookupError: - return abort(404) + try: + groupe = Groupe(uuid=format_uuid(uuid)) + return redirect(f"/groupe/{format_uuid(uuid)}") + except LookupError: + return abort(404) + + groupe.users = [User(user_id=i["id"]) for i in groupe.get_users()] + groupe.get_albums() + user = User(user_id=session.get("user_id")) + + if user.id is None: + # On ne propose pas aux gens non connectés de rejoindre l'album + not_participant = False + else: + not_participant = not user.id in [i.id for i in groupe.users] + + return render_template( + "groupe/index.html", + groupe=groupe, + not_participant=not_participant, + user=user + ) + + +@bp.route("//qr") +def album_qr_code(uuid): + return get_qrcode(f"/groupe/{uuid}") @@ -65,7 +73,7 @@ def create_groupe(): if error is None: while True: try: - uuid = str(uuid4()) + uuid = new_uuid() db.execute( """ @@ -182,7 +190,7 @@ def create_album(groupe_uuid): if error is None: while True: try: - uuid = str(uuid4()) + uuid = new_uuid() db.execute( """ @@ -228,11 +236,19 @@ def album(groupe_uuid, album_uuid): try: groupe = Groupe(uuid=groupe_uuid) except LookupError: - abort(404) + try: + groupe = Groupe(uuid=format_uuid(groupe_uuid)) + return redirect(f"/groupe/{format_uuid(groupe_uuid)}/{album_uuid}") + except LookupError: + return abort(404) album_list = [a for a in groupe.get_albums() if a.uuid == album_uuid] if len(album_list) == 0: - abort(404) + album_uuid = format_uuid(album_uuid) + album_list = [a for a in groupe.get_albums() if a.uuid == album_uuid] + if len(album_list) != 0: + return redirect(f"/groupe/{groupe_uuid}/{album_uuid}") + return abort(404) album = album_list[0] user = User(user_id=session.get("user_id")) @@ -256,4 +272,9 @@ def album(groupe_uuid, album_uuid): partitions=partitions, not_participant=not_participant, user=user - ) \ No newline at end of file + ) + + +@bp.route("///qr") +def groupe_qr_code(groupe_uuid, album_uuid): + return get_qrcode(f"/groupe/{groupe_uuid}/{album_uuid}") \ No newline at end of file diff --git a/partitioncloud/modules/utils.py b/partitioncloud/modules/utils.py index 6239b72..531ff59 100644 --- a/partitioncloud/modules/utils.py +++ b/partitioncloud/modules/utils.py @@ -1,7 +1,29 @@ #!/usr/bin/python3 import os +import io +import random +import string +import qrcode from .db import get_db +from flask import current_app, send_file + + +def new_uuid(): + return ''.join([random.choice(string.ascii_uppercase + string.digits) for _ in range(6)]) + +def format_uuid(uuid): + """Format old uuid4 format""" + return uuid.upper()[:6] + +def get_qrcode(location): + complete_url = current_app.config["BASE_URL"] + location + img_io = io.BytesIO() + + qrcode.make(complete_url).save(img_io) + img_io.seek(0) + return send_file(img_io, mimetype="image/jpeg") + from .classes.user import User from .classes.album import Album diff --git a/partitioncloud/schema.sql b/partitioncloud/schema.sql index 4ef3ccf..f1c48ac 100644 --- a/partitioncloud/schema.sql +++ b/partitioncloud/schema.sql @@ -28,7 +28,7 @@ CREATE TABLE partition ( CREATE TABLE album ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, - uuid TEXT(36) UNIQUE NOT NULL + uuid TEXT(6) UNIQUE NOT NULL ); CREATE TABLE contient_partition ( @@ -52,7 +52,7 @@ CREATE TABLE search_results ( CREATE TABLE groupe ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, - uuid TEXT(36) NOT NULL + uuid TEXT(6) UNIQUE NOT NULL ); CREATE TABLE groupe_contient_user ( diff --git a/partitioncloud/static/style.css b/partitioncloud/static/style.css index 276a1c4..f4a8c6d 100644 --- a/partitioncloud/static/style.css +++ b/partitioncloud/static/style.css @@ -749,4 +749,19 @@ midi-player { .centered { justify-content: center; display: flex; -} \ No newline at end of file +} + +#share-qrcode { + margin: 20px; + margin-top: 50px; + border-radius: 15px; +} + +#share-url { + background-color: var(--color-surface1); + width: fit-content; + border-radius: 4px; + padding: 10px; + margin-bottom: 100px; + margin-top: 20px; +} diff --git a/partitioncloud/templates/albums/album.html b/partitioncloud/templates/albums/album.html index 7d3d15d..1a39796 100644 --- a/partitioncloud/templates/albums/album.html +++ b/partitioncloud/templates/albums/album.html @@ -24,6 +24,15 @@ Close +{% if groupe %} + {% set current_url = "/groupe/" + groupe.uuid + "/" + album.uuid %} +{% else %} + {% set current_url = "/albums/" + album.uuid %} +{% endif %} +{% with share_link=config.BASE_URL+current_url, share_qrlink=current_url + "/qr" %} + {% include 'components/share_dialog.html' %} +{% endwith %} + {% endblock %} {% block content %} @@ -53,6 +62,7 @@ {% elif album.users | length > 1 %} Quitter {% endif %} + Partager {% if g.user.access_level == 1 or (not not_participant and album.users | length == 1) %} Supprimer {% endif %} diff --git a/partitioncloud/templates/components/share_dialog.html b/partitioncloud/templates/components/share_dialog.html new file mode 100644 index 0000000..54548b8 --- /dev/null +++ b/partitioncloud/templates/components/share_dialog.html @@ -0,0 +1,10 @@ + +
+
+
+ {{ share_link }} +
+
+ + Close +
\ No newline at end of file diff --git a/partitioncloud/templates/groupe/index.html b/partitioncloud/templates/groupe/index.html index 68ff5a3..e308a53 100644 --- a/partitioncloud/templates/groupe/index.html +++ b/partitioncloud/templates/groupe/index.html @@ -22,6 +22,10 @@ Close +{% set current_url = "/groupe/" + groupe.uuid %} +{% with share_link=config.BASE_URL+current_url, share_qrlink=current_url + "/qr" %} + {% include 'components/share_dialog.html' %} +{% endwith %} {% endblock %} {% block content %} @@ -44,6 +48,7 @@ {% elif groupe.users | length > 1 %} Quitter {% endif %} + Partager {% if g.user.access_level == 1 or user.id in groupe.get_admins() %} Ajouter un album Supprimer diff --git a/scripts/hooks/utils.py b/scripts/hooks/utils.py index 6dd113b..bdf9c3d 100644 --- a/scripts/hooks/utils.py +++ b/scripts/hooks/utils.py @@ -1,9 +1,27 @@ +import random +import string import sqlite3 -def run_sqlite_command(cmd): +def run_sqlite_command(*args): """Run a command against the database""" con = sqlite3.connect("instance/partitioncloud.sqlite") cur = con.cursor() - cur.execute(cmd) + cur.execute(*args) con.commit() - con.close() \ No newline at end of file + con.close() + +def get_sqlite_data(*args): + """Get data from the db""" + con = sqlite3.connect("instance/partitioncloud.sqlite") + cur = con.cursor() + data = cur.execute(*args) + new_data = [i for i in data] + con.close() + return new_data + +def new_uuid(): + return ''.join([random.choice(string.ascii_uppercase + string.digits) for _ in range(6)]) + +def format_uuid(uuid): + """Format old uuid4 format""" + return uuid.upper()[:6] \ No newline at end of file diff --git a/scripts/hooks/v1.py b/scripts/hooks/v1.py index 3d8f263..2e558c9 100644 --- a/scripts/hooks/v1.py +++ b/scripts/hooks/v1.py @@ -1,6 +1,11 @@ import os +import sqlite3 from hooks import utils +from colorama import Fore, Style +""" + v1.3.* +""" def add_source(): utils.run_sqlite_command( "ALTER TABLE partition ADD source TEXT DEFAULT 'unknown'" @@ -43,4 +48,86 @@ def add_attachments(): ) def install_colorama(): - os.system("pip install colorama -qq") \ No newline at end of file + os.system("pip install colorama -qq") + + +""" + v1.4.* +""" +def mass_rename(): + """Rename all albums & groupes to use a shorter uuid""" + albums = utils.get_sqlite_data("SELECT * FROM album") + groupes = utils.get_sqlite_data("SELECT * FROM groupe") + + utils.run_sqlite_command( + "ALTER TABLE groupe RENAME TO _groupe_old" + ) + utils.run_sqlite_command( + "ALTER TABLE album RENAME TO _album_old" + ) + + utils.run_sqlite_command( # Add UNIQUE constraint & change uuid length + """CREATE TABLE groupe ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + uuid TEXT(6) UNIQUE NOT NULL + );""" + ) + + utils.run_sqlite_command( # Change uuid length + """CREATE TABLE album ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + uuid TEXT(6) UNIQUE NOT NULL + );""" + ) + + for album in albums: + try: + utils.run_sqlite_command( + """ + INSERT INTO album (id, name, uuid) + VALUES (?, ?, ?) + """, + (album[0], album[1], utils.format_uuid(album[2])) + ) + except sqlite3.IntegrityError: + uuid = new_uuid() + print(f"{Fore.RED}Collision on {album[1]}{Style.RESET_ALL} ({album[2][:10]} renaming to {uuid})") + utils.run_sqlite_command( + """ + INSERT INTO album (id, name, uuid) + VALUES (?, ?, ?) + """, + (album[0], album[1], uuid) + ) + + for groupe in groupes: + try: + utils.run_sqlite_command( + """ + INSERT INTO groupe (id, name, uuid) + VALUES (?, ?, ?) + """, + (groupe[0], groupe[1], utils.format_uuid(groupe[2])) + ) + except sqlite3.IntegrityError: + uuid = new_uuid() + print(f"{Fore.RED}Collision on {groupe[1]}{Style.RESET_ALL} ({groupe[2][:10]} renaming to {uuid})") + utils.run_sqlite_command( + """ + INSERT INTO groupe (id, name, uuid) + VALUES (?, ?, ?) + """, + (groupe[0], groupe[1], uuid) + ) + + utils.run_sqlite_command( + "DROP TABLE _groupe_old" + ) + utils.run_sqlite_command( + "DROP TABLE _album_old" + ) + +def base_url_parameter_added(): + print(f"{Style.BRIGHT}{Fore.YELLOW}The parameter BASE_URL has been added, reference your front url in it{Style.RESET_ALL}") \ No newline at end of file diff --git a/scripts/migration.py b/scripts/migration.py index eef1d66..81e6192 100644 --- a/scripts/migration.py +++ b/scripts/migration.py @@ -32,6 +32,10 @@ hooks = [ ]), ("v1.3.3", [ ("Install colorama", v1.install_colorama) + ]), + ("v1.4.0", [ + ("Change all albums & groupes uuids", v1.mass_rename), + ("Warn new parameter", v1.base_url_parameter_added) ]) ] @@ -60,7 +64,7 @@ def backup_instance(version, verbose=True): if verbose: print(*args) - print_verbose("Backing up current instance") + print_verbose("\nBacking up current instance") dest = os.path.join("backups", version) if os.path.exists(dest): @@ -95,7 +99,7 @@ def apply_hooks(hooks): subhook[1]() -def migrate(current, target, skip_backup=False): +def migrate(current, target, skip_backup=False, prog_name="scripts/migration.py"): """""" print(f"Trying to migrate from {args.current} to {args.target}") @@ -115,7 +119,8 @@ def migrate(current, target, skip_backup=False): if not skip_backup: backup_instance(current) - print(f"Instance backed up in {Style.BRIGHT}backups/{current}{Style.RESET_ALL}") + print(f"Instance backed up in {Style.BRIGHT}backups/{current}{Style.RESET_ALL}\n") + print(f"If something goes wrong, recover with {Style.BRIGHT}{Fore.BLUE}{prog_name} --restore {current}{Style.RESET_ALL}") else: print("Skipping automatic backup")