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. diff --git a/simple_viewer/settings.py b/simple_viewer/settings.py index 4b2d56c..a412387 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 = 'US/Central' USE_I18N = True diff --git a/viewer/helpers.py b/viewer/helpers.py new file mode 100644 index 0000000..02cf123 --- /dev/null +++ b/viewer/helpers.py @@ -0,0 +1,35 @@ +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'): + return 'image' + elif mimetype.startswith('video'): + return 'video' + return 'file' 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..9259d89 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(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='') + regex = models.BooleanField('Directory RegEx Option', default=False) + match_filename = models.BooleanField('RegEx Matches Against Filename', default=True) + + def __str__(self): + return self.path 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/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..dde12aa --- /dev/null +++ b/viewer/templates/browse.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} +{% block content %} +
+
+ Files + + {{ files|length }} + + + + + + +
+ {% for file in files %} +
+ + + + + {{ file.0 }} + +
+ {% endfor %} +
+ + {% load static %} + +{% 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 %} +
+ + + + + {{ served_directory.path }} + +
+ {% 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..2b2acb8 100644 --- a/viewer/urls.py +++ b/viewer/urls.py @@ -3,5 +3,7 @@ from django.urls import path from . import views urlpatterns = [ - path('', views.index, name='index') + 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 e00860a..00b7aa3 100644 --- a/viewer/views.py +++ b/viewer/views.py @@ -1,7 +1,56 @@ -from django.shortcuts import render +import os + +from django.http import FileResponse +from django.shortcuts import render, get_object_or_404 + +from viewer.helpers import extra_listdir +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): + context = { + 'title': f'Browse - {os.path.dirname(dir.path)}', + 'files': extra_listdir(dir.path), + '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) + + +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)