From 77cbae0a543401f88c0065c75981b22c7ffd3d33 Mon Sep 17 00:00:00 2001 From: Xevion Date: Sat, 31 Oct 2020 05:03:25 -0500 Subject: [PATCH 1/6] add README & .gitignore --- .gitignore | 142 +++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 9 ++++ 2 files changed, 151 insertions(+) create mode 100644 .gitignore create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..43660d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,142 @@ +# Repository specific +.idea/** + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4bc9c9 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# simple-viewer + +Simple Viewer is a small learning project designed to work as a static file server with accompanying viewing methods. + +These viewing methods are intended to be as simple as possible, mostly focusing on video. + +Planned are text files, source code files, PDFs, and pictures. Everything else will be served as direct browser links (the browser handles all viewing) or as downloads. + +The project will be built in Django (as well as HTML/CSS/JS), my first ever project in the framework, so expect poorer standards, mishaps, some complex bugs (resolved with needlessly complex solutions) and such. From ec785d536046fd50376a27741c0bbfbce2e5709d Mon Sep 17 00:00:00 2001 From: Xevion Date: Sat, 31 Oct 2020 05:28:25 -0500 Subject: [PATCH 2/6] create ServedDirectory model, setup project settings, add Viewer app to project, uuid migration --- simple_viewer/settings.py | 3 ++- viewer/migrations/0001_initial.py | 25 ++++++++++++++++++++ viewer/migrations/0002_auto_20201031_0526.py | 19 +++++++++++++++ viewer/models.py | 23 +++++++++++++++++- 4 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 viewer/migrations/0001_initial.py create mode 100644 viewer/migrations/0002_auto_20201031_0526.py diff --git a/simple_viewer/settings.py b/simple_viewer/settings.py index 4b2d56c..4d87206 100644 --- a/simple_viewer/settings.py +++ b/simple_viewer/settings.py @@ -31,6 +31,7 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ + 'viewer.apps.ViewerConfig', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -106,7 +107,7 @@ AUTH_PASSWORD_VALIDATORS = [ LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' +TIME_ZONE = 'CST' USE_I18N = True diff --git a/viewer/migrations/0001_initial.py b/viewer/migrations/0001_initial.py new file mode 100644 index 0000000..666422f --- /dev/null +++ b/viewer/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# Generated by Django 3.1.2 on 2020-10-31 10:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='ServedDirectory', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('path', models.CharField(max_length=260, verbose_name='Directory Path')), + ('recursive', models.BooleanField(default=False, verbose_name='Files Are Matched Recursively')), + ('regex_pattern', models.CharField(default=None, max_length=100, verbose_name='RegEx Matching Pattern')), + ('regex', models.BooleanField(default=False, verbose_name='Directory RegEx Option')), + ('match_filename', models.BooleanField(default=True, verbose_name='RegEx Matches Against Filename')), + ], + ), + ] diff --git a/viewer/migrations/0002_auto_20201031_0526.py b/viewer/migrations/0002_auto_20201031_0526.py new file mode 100644 index 0000000..9cc150c --- /dev/null +++ b/viewer/migrations/0002_auto_20201031_0526.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.2 on 2020-10-31 10:26 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('viewer', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='serveddirectory', + name='id', + field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True), + ), + ] diff --git a/viewer/models.py b/viewer/models.py index 71a8362..aa378c7 100644 --- a/viewer/models.py +++ b/viewer/models.py @@ -1,3 +1,24 @@ +import uuid + from django.db import models -# Create your models here. + +class ServedDirectory(models.Model): + """ + A reference to a specific directory on the host machine for hosting files. + + A regex pattern is stored for filtering files in the directory down to what is intended. + A recursive option is also stored, in case the user wishes to serve files in directories below the one specified. + The regex pattern can be turned on or off using the boolean field. + The regex pattern can be matched against the file path (False), or just the filename (True). + """ + + id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, unique=True) + path = models.CharField('Directory Path', max_length=260) + recursive = models.BooleanField('Files Are Matched Recursively', default=False) + regex_pattern = models.CharField('RegEx Matching Pattern', max_length=100, default=None) + regex = models.BooleanField('Directory RegEx Option', default=False) + match_filename = models.BooleanField('RegEx Matches Against Filename', default=True) + + def __str__(self): + return self.path From f7cde0b4dc7cc15a88ce7951dec3e98aae07eced Mon Sep 17 00:00:00 2001 From: Xevion Date: Sat, 31 Oct 2020 12:26:55 -0500 Subject: [PATCH 3/6] fix timezone setting, fix invalid default for CharField --- simple_viewer/settings.py | 2 +- viewer/models.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/simple_viewer/settings.py b/simple_viewer/settings.py index 4d87206..a412387 100644 --- a/simple_viewer/settings.py +++ b/simple_viewer/settings.py @@ -107,7 +107,7 @@ AUTH_PASSWORD_VALIDATORS = [ LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'CST' +TIME_ZONE = 'US/Central' USE_I18N = True diff --git a/viewer/models.py b/viewer/models.py index aa378c7..9259d89 100644 --- a/viewer/models.py +++ b/viewer/models.py @@ -13,10 +13,10 @@ class ServedDirectory(models.Model): The regex pattern can be matched against the file path (False), or just the filename (True). """ - id = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, unique=True) + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, unique=True) path = models.CharField('Directory Path', max_length=260) recursive = models.BooleanField('Files Are Matched Recursively', default=False) - regex_pattern = models.CharField('RegEx Matching Pattern', max_length=100, default=None) + regex_pattern = models.CharField('RegEx Matching Pattern', max_length=100, default='') regex = models.BooleanField('Directory RegEx Option', default=False) match_filename = models.BooleanField('RegEx Matches Against Filename', default=True) From c8fcb09b73311dfcb768bae33fedb0e5d4a5408b Mon Sep 17 00:00:00 2001 From: Xevion Date: Sat, 31 Oct 2020 12:28:29 -0500 Subject: [PATCH 4/6] basic site design, directory path reading, uuid setup, file icon mimetype categorization --- viewer/helpers.py | 8 ++++++++ viewer/templates/base.html | 37 ++++++++++++++++++++++++++++++++++ viewer/templates/browse.html | 24 ++++++++++++++++++++++ viewer/templates/index.html | 24 ++++++++++++++++++++++ viewer/templates/message.html | 15 ++++++++++++++ viewer/urls.py | 3 ++- viewer/views.py | 38 ++++++++++++++++++++++++++++++++--- 7 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 viewer/helpers.py create mode 100644 viewer/templates/base.html create mode 100644 viewer/templates/browse.html create mode 100644 viewer/templates/index.html create mode 100644 viewer/templates/message.html diff --git a/viewer/helpers.py b/viewer/helpers.py new file mode 100644 index 0000000..a073401 --- /dev/null +++ b/viewer/helpers.py @@ -0,0 +1,8 @@ +def get_mediatype(mimetype: str) -> str: + """Simple media type categorization based on the given mimetype""" + if mimetype is not None: + if mimetype.startswith('image'): + return 'image' + elif mimetype.startswith('video'): + return 'video' + return 'file' diff --git a/viewer/templates/base.html b/viewer/templates/base.html new file mode 100644 index 0000000..e03a51c --- /dev/null +++ b/viewer/templates/base.html @@ -0,0 +1,37 @@ + + + + {% block head %} + + + {{ title }} + + {% endblock head %} + + + +
+
+ {% block content %} + {% endblock %} +
+
+ + + diff --git a/viewer/templates/browse.html b/viewer/templates/browse.html new file mode 100644 index 0000000..eebe1ae --- /dev/null +++ b/viewer/templates/browse.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} +{% block content %} +
+
+ Files + + {{ files|length }} + + + + + + +
+ {% for file in files %} +
+ + + + {{ file.0 }} +
+ {% endfor %} +
+{% endblock content %} diff --git a/viewer/templates/index.html b/viewer/templates/index.html new file mode 100644 index 0000000..e07255f --- /dev/null +++ b/viewer/templates/index.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} +{% block head %} + {{ block.super }} + +{% endblock head %} +{% block content %} +
+
Directories
+ {% for served_directory in directories %} + + {% endfor %} +
+{% endblock content %} diff --git a/viewer/templates/message.html b/viewer/templates/message.html new file mode 100644 index 0000000..d3be1c6 --- /dev/null +++ b/viewer/templates/message.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% block content %} +
+
+

+ {{ title }} +

+
+
+
+ {{ message }} +
+
+
+{% endblock content %} diff --git a/viewer/urls.py b/viewer/urls.py index 2fcca0d..c6bf29a 100644 --- a/viewer/urls.py +++ b/viewer/urls.py @@ -3,5 +3,6 @@ from django.urls import path from . import views urlpatterns = [ - path('', views.index, name='index') + path('', views.index, name='index'), + path('//', views.browse, name='browse'), ] diff --git a/viewer/views.py b/viewer/views.py index e00860a..ae250e2 100644 --- a/viewer/views.py +++ b/viewer/views.py @@ -1,7 +1,39 @@ -from django.shortcuts import render +import mimetypes +import os + +from django.http import FileResponse +from django.shortcuts import render, get_object_or_404 + +from viewer.helpers import get_mediatype +from viewer.models import ServedDirectory -from django.http import HttpResponse def index(request): """Index view for the simple-viewer project.""" - return HttpResponse('Hello, World.') + directories = ServedDirectory.objects.all() + context = {'title': 'Index', + 'directories': directories} + return render(request, 'index.html', context) + + +def browse(request, directory_id): + dir = get_object_or_404(ServedDirectory, id=directory_id) + + if os.path.isdir(dir.path): + files = [ + (file, get_mediatype(mimetypes.guess_type(file)[0])) for file in os.listdir(dir.path) + ] + context = { + 'title': f'Browse - {os.path.dirname(dir.path)}', + 'files': files, + 'directory': dir + } + return render(request, 'browse.html', context) + else: + context = { + 'title': 'Invalid Directory', + 'message': 'The path this server directory points to {}.'.format( + 'exists, but is not a directory' if os.path.exists(dir.path) else 'does not exist' + ) + } + return render(request, 'message.html', context, status=500) From 8cbf8396fd7cd6daf5a64406a7af5585a2801fdb Mon Sep 17 00:00:00 2001 From: Xevion Date: Sat, 31 Oct 2020 12:39:28 -0500 Subject: [PATCH 5/6] basic file sending/viewing --- viewer/templates/browse.html | 4 +++- viewer/urls.py | 1 + viewer/views.py | 21 +++++++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/viewer/templates/browse.html b/viewer/templates/browse.html index eebe1ae..33ada3c 100644 --- a/viewer/templates/browse.html +++ b/viewer/templates/browse.html @@ -17,7 +17,9 @@ - {{ file.0 }} + + {{ file.0 }} + {% endfor %} diff --git a/viewer/urls.py b/viewer/urls.py index c6bf29a..2b2acb8 100644 --- a/viewer/urls.py +++ b/viewer/urls.py @@ -5,4 +5,5 @@ from . import views urlpatterns = [ path('', views.index, name='index'), path('//', views.browse, name='browse'), + path('///', views.file, name='file') ] diff --git a/viewer/views.py b/viewer/views.py index ae250e2..98c7d8c 100644 --- a/viewer/views.py +++ b/viewer/views.py @@ -37,3 +37,24 @@ def browse(request, directory_id): ) } return render(request, 'message.html', context, status=500) + + +def file(request, directory_id, file): + dir = get_object_or_404(ServedDirectory, id=directory_id) + if os.path.isdir(dir.path): + path = os.path.join(dir.path, file) + if os.path.exists(path): + return FileResponse(open(path, 'rb')) + else: + context = { + 'title': 'Invalid File', + 'message': 'The file requested from this directory was not found on the server.' + } + return render(request, 'message.html', context, status=500) + context = { + 'title': 'Invalid Directory', + 'message': 'The path this server directory points to {}.'.format( + 'exists, but is not a directory' if os.path.exists(dir.path) else 'does not exist' + ) + } + return render(request, 'message.html', context, status=500) From c53e8ecf9c2bda7b35a37f19ed2df872cb751b8c Mon Sep 17 00:00:00 2001 From: Xevion Date: Sat, 31 Oct 2020 17:14:20 -0500 Subject: [PATCH 6/6] add directory mediatype functionality via helpers, begin looking into static jquery (nothing yet) --- viewer/helpers.py | 29 ++++++++++++++++++++++++++++- viewer/static/hover.js | 1 + viewer/templates/browse.html | 5 ++++- viewer/views.py | 8 ++------ 4 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 viewer/static/hover.js diff --git a/viewer/helpers.py b/viewer/helpers.py index a073401..02cf123 100644 --- a/viewer/helpers.py +++ b/viewer/helpers.py @@ -1,4 +1,31 @@ -def get_mediatype(mimetype: str) -> str: +import os +from typing import Tuple, List + + +def extra_listdir(path: str) -> List[Tuple[str, str]]: + """ + Helper function used for identifying file media type for every file in a given directory, extending os.listdir + + :param path: The path to the directory. + :return: A list of tuples, each containing two strings, the file or directory name, and the media type. + """ + return [(file, get_all_mediatype(file, path)) for file in os.listdir(path)] + + +def get_all_mediatype(head: str, tail: str) -> str: + """ + A extra media type function supporting directories on top of files. + + :param head: The head of the path, usually the directory name or filename at the very end. + :param tail: The rest of the path, everything that comes before the head. + :return: A media type in string form. + """ + if os.path.isfile(os.path.join(tail, head)): + return get_file_mediatype(head) + return "folder" + + +def get_file_mediatype(mimetype: str) -> str: """Simple media type categorization based on the given mimetype""" if mimetype is not None: if mimetype.startswith('image'): diff --git a/viewer/static/hover.js b/viewer/static/hover.js new file mode 100644 index 0000000..0247eeb --- /dev/null +++ b/viewer/static/hover.js @@ -0,0 +1 @@ +asdas diff --git a/viewer/templates/browse.html b/viewer/templates/browse.html index 33ada3c..dde12aa 100644 --- a/viewer/templates/browse.html +++ b/viewer/templates/browse.html @@ -17,10 +17,13 @@ - + {{ file.0 }} {% endfor %} + + {% load static %} + {% endblock content %} diff --git a/viewer/views.py b/viewer/views.py index 98c7d8c..00b7aa3 100644 --- a/viewer/views.py +++ b/viewer/views.py @@ -1,10 +1,9 @@ -import mimetypes import os from django.http import FileResponse from django.shortcuts import render, get_object_or_404 -from viewer.helpers import get_mediatype +from viewer.helpers import extra_listdir from viewer.models import ServedDirectory @@ -20,12 +19,9 @@ def browse(request, directory_id): dir = get_object_or_404(ServedDirectory, id=directory_id) if os.path.isdir(dir.path): - files = [ - (file, get_mediatype(mimetypes.guess_type(file)[0])) for file in os.listdir(dir.path) - ] context = { 'title': f'Browse - {os.path.dirname(dir.path)}', - 'files': files, + 'files': extra_listdir(dir.path), 'directory': dir } return render(request, 'browse.html', context)