diff --git a/.gitignore b/.gitignore index 33a5696..ad7bae3 100644 --- a/.gitignore +++ b/.gitignore @@ -89,6 +89,8 @@ ENV/ # Rope project settings .ropeproject +# Local development files +localconfig.env.py config.py data.db @@ -97,3 +99,6 @@ data.db # mypy .mypy_cache + +# vim swap files +*.swp diff --git a/README.md b/README.md index 324ed05..40b0bd4 100644 --- a/README.md +++ b/README.md @@ -65,3 +65,18 @@ an Openshift Origin cluster. GALLERY_S3_BUCKET_ID = $s3BucketID GALLERY_S3_SECRET_KEY = $s3SecretKey ``` + +## Local Development +Below are instructions for running gallery locally. It assumes that you have already forked and cloned this repository onto your local machine, and have Python3 installed. + +1. Change the line in `__init__.py` that sets the config file from `config.env.py` to `localconfig.env.py`. + +2. Copy `localconfig-sample.env.py` to `localconfig.env.py`, get gallery dev secrets from an RTP, and fill in. + +3. Create a [virtual environment](https://docs.python.org/3/library/venv.html), `python3 -m venv venv` + +4. `source venv/bin/activate` to enter the virtual environment + +5. `pip install -r requirements.txt` + +6. `python3 wsgi.py` diff --git a/gallery/__init__.py b/gallery/__init__.py index 8ea5fcc..2fc5925 100644 --- a/gallery/__init__.py +++ b/gallery/__init__.py @@ -25,6 +25,7 @@ from flask import send_from_directory from flask import abort from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import or_ from sqlalchemy.sql import func as sql_func from sqlalchemy.orm import load_only from flask_pyoidc.flask_pyoidc import OIDCAuthentication @@ -213,7 +214,7 @@ def upload_file(auth_dict: Optional[Dict[str, Any]] = None): upload_status['error'] = errors upload_status['success'] = success - refresh_thumbnail() + refresh_default_thumbnails() # actually redirect to URL # change from FORM post to AJAX maybe? return jsonify(upload_status) @@ -246,6 +247,9 @@ def view_mkdir(auth_dict: Optional[Dict[str, Any]] = None): @auth.oidc_auth('default') @gallery_auth def view_jumpdir(auth_dict: Optional[Dict[str, Any]] = None): + gallery_lockdown = util.get_lockdown_status() + if gallery_lockdown and (not auth_dict['is_eboard'] and not auth_dict['is_rtp']): + abort(405) return render_template("jumpdir.html", auth_dict=auth_dict) @@ -311,7 +315,7 @@ def api_mkdir( @app.cli.command() def refresh_thumbnails(): click.echo("Refreshing thumbnails") - refresh_thumbnail() + refresh_default_thumbnails() @app.cli.command() @@ -386,24 +390,25 @@ def add_file(file_name: str, path: str, dir_id: str, description: str, owner: st return file_model -def refresh_thumbnail(): - def refresh_thumbnail_helper(dir_model: Directory) -> str: - dir_children = [d for d in Directory.query.filter(Directory.parent == dir_model.id).all()] - file_children = [f for f in File.query.filter(File.parent == dir_model.id).all()] - for file in file_children: - if file.thumbnail_uuid != DEFAULT_THUMBNAIL_NAME: - return file.thumbnail_uuid - for d in dir_children: - if d.thumbnail_uuid != DEFAULT_THUMBNAIL_NAME: - return d.thumbnail_uuid - # WE HAVE TO GO DEEPER (inception noise) - for d in dir_children: - # TODO: Switch to iterative tree walk using a queue to avoid - # recursion issues with super large directory structures - return refresh_thumbnail_helper(d) - # No thumbnail found - return DEFAULT_THUMBNAIL_NAME - +def refresh_directory_thumbnail(dir_model: Directory) -> str: + dir_children = [d for d in Directory.query.filter(Directory.parent == dir_model.id).all()] + file_children = [f for f in File.query.filter(File.parent == dir_model.id).all()] + for file in file_children: + if file.thumbnail_uuid != DEFAULT_THUMBNAIL_NAME and not file.hidden: + return file.thumbnail_uuid + for d in dir_children: + if d.thumbnail_uuid != DEFAULT_THUMBNAIL_NAME: + return d.thumbnail_uuid + # WE HAVE TO GO DEEPER (inception noise) + for d in dir_children: + # TODO: Switch to iterative tree walk using a queue to avoid + # recursion issues with super large directory structures + return refresh_directory_thumbnail(d) + # No thumbnail found + return DEFAULT_THUMBNAIL_NAME + + +def refresh_default_thumbnails(): missing_thumbnails = File.query.filter(File.thumbnail_uuid == DEFAULT_THUMBNAIL_NAME).all() for file_model in missing_thumbnails: dir_path = get_full_dir_path(file_model.parent) @@ -416,7 +421,7 @@ def refresh_thumbnail_helper(dir_model: Directory) -> str: missing_thumbnails = Directory.query.filter(Directory.thumbnail_uuid == DEFAULT_THUMBNAIL_NAME).all() for dir_model in missing_thumbnails: - dir_model.thumbnail_uuid = refresh_thumbnail_helper(dir_model) + dir_model.thumbnail_uuid = refresh_directory_thumbnail(dir_model) db.session.flush() db.session.commit() db.session.refresh(dir_model) @@ -476,6 +481,13 @@ def hide_file(file_id: int, auth_dict: Optional[Dict[str, Any]] = None): return "Permission denied", 403 file_model.hidden = True + + # Remove image from thumbnails + dirs = Directory.query.filter(or_(Directory.thumbnail_uuid == file_model.thumbnail_uuid, \ + Directory.thumbnail_uuid == file_model.thumbnail_uuid[:-4])) + for d in dirs: + d.thumbnail_uuid = refresh_directory_thumbnail(d) + db.session.flush() db.session.commit() @@ -497,6 +509,12 @@ def show_file(file_id: int, auth_dict: Optional[Dict[str, Any]] = None): return "Permission denied", 403 file_model.hidden = False + + # Add image as directory thumbnail + parent_model = Directory.query.filter(Directory.id == file_model.parent).first() + if parent_model.thumbnail_uuid == DEFAULT_THUMBNAIL_NAME: + parent_model.thumbnail_uuid = refresh_directory_thumbnail(parent_model) + db.session.flush() db.session.commit() @@ -718,7 +736,12 @@ def tag_file(file_id: int): @app.route("/api/file/get/") @auth.oidc_auth('default') -def display_file(file_id: int): +@gallery_auth +def display_file(file_id: int, auth_dict: Optional[Dict[str, Any]] = None): + gallery_lockdown = util.get_lockdown_status() + if gallery_lockdown and (not auth_dict['is_eboard'] and not auth_dict['is_rtp']): + abort(405) + file_model = File.query.filter(File.id == file_id).first() if file_model is None: @@ -730,7 +753,12 @@ def display_file(file_id: int): @app.route("/api/thumbnail/get/") @auth.oidc_auth('default') -def display_thumbnail(file_id: int): +@gallery_auth +def display_thumbnail(file_id: int, auth_dict: Optional[Dict[str, Any]] = None): + gallery_lockdown = util.get_lockdown_status() + if gallery_lockdown and (not auth_dict['is_eboard'] and not auth_dict['is_rtp']): + abort(405) + file_model = File.query.filter(File.id == file_id).first() link = storage_interface.get_link("thumbnails/{}".format(file_model.s3_id)) @@ -739,7 +767,12 @@ def display_thumbnail(file_id: int): @app.route("/api/thumbnail/get/dir/") @auth.oidc_auth('default') -def display_dir_thumbnail(dir_id: int): +@gallery_auth +def display_dir_thumbnail(dir_id: int, auth_dict: Optional[Dict[str, Any]] = None): + gallery_lockdown = util.get_lockdown_status() + if gallery_lockdown and (not auth_dict['is_eboard'] and not auth_dict['is_rtp']): + abort(405) + dir_model = Directory.query.filter(Directory.id == dir_id).first() thumbnail_uuid = dir_model.thumbnail_uuid @@ -795,7 +828,11 @@ def get_supported_mimetypes(): @app.route("/api/get_dir_tree") @auth.oidc_auth('default') -def get_dir_tree(internal: bool = False): +@gallery_auth +def get_dir_tree(internal: bool = False, auth_dict: Optional[Dict[str, Any]] = None): + gallery_lockdown = util.get_lockdown_status() + if gallery_lockdown and (not auth_dict['is_eboard'] and not auth_dict['is_rtp']): + abort(405) # TODO: Convert to iterative tree traversal using a queue to avoid # recursion issues with large directory structures @@ -828,7 +865,12 @@ def get_dir_children(dir_id: int) -> Any: @app.route("/api/directory/get/") @auth.oidc_auth('default') -def display_files(dir_id: int, internal: bool = False): +@gallery_auth +def display_files(dir_id: int, internal: bool = False, auth_dict: Optional[Dict[str, Any]] = None): + gallery_lockdown = util.get_lockdown_status() + if gallery_lockdown and (not auth_dict['is_eboard'] and not auth_dict['is_rtp']): + abort(405) + file_list = [("File", f) for f in File.query.filter(File.parent == dir_id).all()] dir_list = [("Directory", d) for d in Directory.query.filter(Directory.parent == dir_id).all()] @@ -980,7 +1022,12 @@ def view_filtered(auth_dict: Optional[Dict[str, Any]] = None): @app.route("/api/memberlist") @auth.oidc_auth('default') -def get_member_list(): +@gallery_auth +def get_member_list(auth_dict: Optional[Dict[str, Any]] = None): + gallery_lockdown = util.get_lockdown_status() + if gallery_lockdown and (not auth_dict['is_eboard'] and not auth_dict['is_rtp']): + abort(405) + return jsonify(ldap.get_members()) @@ -999,7 +1046,7 @@ def route_errors(error: Any, auth_dict: Optional[Dict[str, Any]] = None): if code == 404: error_desc = "Page Not Found" elif code == 405: - error_desc = "Page Not Available" + error_desc = "Gallery is currently unavailable" else: error_desc = type(error).__name__ diff --git a/gallery/_version.py b/gallery/_version.py index 125c525..f67c258 100644 --- a/gallery/_version.py +++ b/gallery/_version.py @@ -1,6 +1,6 @@ from os import environ as env -__version__ = "2.1.0" +__version__ = "2.1.1" BUILD_REFERENCE = env.get("OPENSHIFT_BUILD_REFERENCE") COMMIT_HASH = env.get("OPENSHIFT_BUILD_COMMIT") diff --git a/gallery/file_modules/__init__.py b/gallery/file_modules/__init__.py index 3ac7f11..39917ab 100644 --- a/gallery/file_modules/__init__.py +++ b/gallery/file_modules/__init__.py @@ -62,6 +62,8 @@ def generate_thumbnail(self): from gallery.file_modules.ogg import OggFile from gallery.file_modules.pdf import PDFFile from gallery.file_modules.txt import TXTFile +from gallery.file_modules.mp3 import MP3File +from gallery.file_modules.wav import WAVFile file_mimetype_relation = { "image/jpeg": JPEGFile, @@ -77,7 +79,9 @@ def generate_thumbnail(self): "video/webm": WebMFile, "video/ogg": OggFile, "application/pdf": PDFFile, - "text/plain": TXTFile + "text/plain": TXTFile, + "audio/mpeg": MP3File, + "audio/x-wav": WAVFile } diff --git a/gallery/file_modules/mp3.py b/gallery/file_modules/mp3.py new file mode 100644 index 0000000..6fb4c9b --- /dev/null +++ b/gallery/file_modules/mp3.py @@ -0,0 +1,18 @@ +import os +from wand.image import Image + +from gallery.file_modules import FileModule +from gallery.util import hash_file + +class MP3File(FileModule): + def __init__(self, file_path, dir_path): + FileModule.__init__(self, file_path, dir_path) + self.mime_type = "audio/mpeg" + + self.generate_thumbnail() + + def generate_thumbnail(self): + self.thumbnail_uuid = hash_file(self.file_path) + ".jpg" + + with Image(filename="thumbnails/reedphoto.jpg") as bg: + bg.save(filename=os.path.join(self.dir_path, self.thumbnail_uuid)) diff --git a/gallery/file_modules/wav.py b/gallery/file_modules/wav.py new file mode 100644 index 0000000..71d251f --- /dev/null +++ b/gallery/file_modules/wav.py @@ -0,0 +1,18 @@ +import os +from wand.image import Image + +from gallery.file_modules import FileModule +from gallery.util import hash_file + +class WAVFile(FileModule): + def __init__(self, file_path, dir_path): + FileModule.__init__(self, file_path, dir_path) + self.mime_type = "audio/x-wav" + + self.generate_thumbnail() + + def generate_thumbnail(self): + self.thumbnail_uuid = hash_file(self.file_path) + ".jpg" + + with Image(filename="thumbnails/reedphoto.jpg") as bg: + bg.save(filename=os.path.join(self.dir_path, self.thumbnail_uuid)) diff --git a/gallery/static/images/material_lock.svg b/gallery/static/images/material_lock.svg new file mode 100644 index 0000000..cc19cec --- /dev/null +++ b/gallery/static/images/material_lock.svg @@ -0,0 +1 @@ + diff --git a/gallery/static/js/gallery.js b/gallery/static/js/gallery.js index b4b44a4..9bcbb79 100644 --- a/gallery/static/js/gallery.js +++ b/gallery/static/js/gallery.js @@ -252,7 +252,7 @@ function hideFile() { function showFile() { $('#show').modal('show'); - $('#show button[id^="hide"]').click(function(e) { + $('#show button[id^="show"]').click(function(e) { e.preventDefault(); var this_id = $('#show button[id^="show"]').attr('id').substr($('#show button[id^="show"]').attr('id').indexOf("-") + 1); $.ajax({ diff --git a/gallery/templates/errors.html b/gallery/templates/errors.html index e5204ee..bc5a7ee 100644 --- a/gallery/templates/errors.html +++ b/gallery/templates/errors.html @@ -6,10 +6,15 @@ {% block body %}
- Attention! -

Oops!

-

Something has gone terribly wrong!

-

{{ error }}

+ {% if error_code == 405 %} + Locked +

{{ error }}

+ {% else %} + Attention +

Oops!

+

Something has gone terribly wrong!

+

{{ error }}

+ {% endif %}
{% endblock %} diff --git a/gallery/templates/view_file.html b/gallery/templates/view_file.html index 0864e77..828a794 100644 --- a/gallery/templates/view_file.html +++ b/gallery/templates/view_file.html @@ -64,6 +64,12 @@ {% elif file.mimetype == "application/pdf" or file.mimetype == "text/plain" %} + + {% elif file.mimetype.split('/')[0] == "audio" %} + + {% else %} Text Data {% endif %} diff --git a/localconfig-sample.env.py b/localconfig-sample.env.py new file mode 100644 index 0000000..e61e250 --- /dev/null +++ b/localconfig-sample.env.py @@ -0,0 +1,27 @@ +import os + +# Flask config +DEBUG=False +IP=os.environ.get('GALLERY_IP', 'localhost') +PORT=os.environ.get('GALLERY_PORT', '6969') +SERVER_NAME = os.environ.get('GALLERY_SERVER_NAME', 'localhost:6969') +SECRET_KEY = os.environ.get('GALLERY_SECRET_KEY', '') + +# LDAP config +LDAP_BIND_DN=os.environ.get('GALLERY_LDAP_BIND_DN', '') +LDAP_BIND_PW=os.environ.get('GALLERY_LDAP_BIND_PW', '') + +# OpenID Connect SSO config +OIDC_ISSUER = os.environ.get('GALLERY_OIDC_ISSUER', 'https://sso.csh.rit.edu/auth/realms/csh') +OIDC_CLIENT_ID = os.environ.get('GALLERY_OIDC_CLIENT_ID', 'gallery-dev') +OIDC_CLIENT_SECRET = os.environ.get('GALLERY_OIDC_CLIENT_SECRET', '') + +SQLALCHEMY_DATABASE_URI = os.environ.get( + 'GALLERY_DATABASE_URI', + 'postgresql://DB_USERNAME:DB_PASSWORD@postgres.csh.rit.edu/gallery-dev') +SQLALCHEMY_TRACK_MODIFICATIONS = False + +S3_URI = os.environ.get('GALLERY_S3_URI', 'https://s3.csh.rit.edu') +S3_ACCESS_ID = os.environ.get('GALLERY_S3_ACCESS_ID','') +S3_SECRET_KEY = os.environ.get('GALLERY_S3_SECRET_KEY','') +S3_BUCKET_ID = os.environ.get('GALLERY_S3_BUCKET_ID','gallery-dev') diff --git a/requirements.txt b/requirements.txt index de06ac5..aae44a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ Flask==1.0.2 Flask-pyoidc==2.0.0 -csh_ldap==2.1.1 +csh_ldap~=2.2.0 addict==2.2.0 flask_sqlalchemy==2.3.2 flask_migrate==2.3.1 diff --git a/setup.cfg b/setup.cfg index 18f1f67..eb28da7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,7 @@ summary = Python Photo Gallery Written in Flask url = "https://github.com/ComputerScienceHouse/gallery" description-file = README.md license = MIT -version = 2.0.5 +version = 2.1.1 classifier = Natural Language :: English Operating System :: POSIX :: Linux