Merge pull request #1 from Xevion/basics

Basics
This commit is contained in:
Xevion
2020-10-31 17:15:14 -05:00
committed by GitHub
14 changed files with 415 additions and 6 deletions

142
.gitignore vendored Normal file
View File

@@ -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/

9
README.md Normal file
View File

@@ -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.

View File

@@ -31,6 +31,7 @@ ALLOWED_HOSTS = []
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'viewer.apps.ViewerConfig',
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
@@ -106,7 +107,7 @@ AUTH_PASSWORD_VALIDATORS = [
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC' TIME_ZONE = 'US/Central'
USE_I18N = True USE_I18N = True

35
viewer/helpers.py Normal file
View File

@@ -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'

View File

@@ -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')),
],
),
]

View File

@@ -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),
),
]

View File

@@ -1,3 +1,24 @@
import uuid
from django.db import models 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

1
viewer/static/hover.js Normal file
View File

@@ -0,0 +1 @@
asdas

View File

@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html>
<head>
{% block head %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title }}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css">
{% endblock head %}
</head>
<body class="has-navbar-fixed-top">
<nav class="navbar is-fixed-top" role="navigation" aria-label="main navigation">
<div class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item" href="{% url 'index' %}">
Index
</a>
</div>
<div class="navbar-end">
<div class="navbar-item">
<span class="icon">
<i class="fas fa-plus"></i>
</span>
</div>
</div>
</div>
</nav>
<div class="columns is-centered my-5">
<div class="column is-two-thirds">
{% block content %}
{% endblock %}
</div>
</div>
<script defer src="https://use.fontawesome.com/releases/v5.14.0/js/all.js"></script>
</body>
</html>

View File

@@ -0,0 +1,29 @@
{% extends 'base.html' %}
{% block content %}
<div class="panel">
<div class="panel-heading">
Files
<span style="font-weight: 400; font-style: italic; font-size: 70%;">
{{ files|length }}
</span>
<span style="vertical-align: middle;" class="panel-icon">
<a href="{% url 'index' %}">
<i class="fas fa-arrow-up" aria-hidden="true"></i>
</a>
</span>
</div>
{% for file in files %}
<div class="panel-block">
<span class="panel-icon pr-4">
<i class="fas fa-{{ file.1 }} fa-lg" aria-hidden="true"></i>
</span>
<a href="{% if file.1 != 'folder' %}{% url 'file' directory.id file.0 %}{% endif %}">
{{ file.0 }}
</a>
</div>
{% endfor %}
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js" integrity="sha512-bLT0Qm9VnAYZDflyKcBaQ2gg0hSYNQrJ8RilYldYQ1FxQYoCLtUjuuRuZo+fjqhx/qtq/1itJ0C2ejDxltZVFg==" crossorigin="anonymous"></script>
{% load static %}
<script src="{% static "hover.js" %}"></script>
{% endblock content %}

View File

@@ -0,0 +1,24 @@
{% extends 'base.html' %}
{% block head %}
{{ block.super }}
<style>
.panel-icon {
margin-right: 1em;
}
</style>
{% endblock head %}
{% block content %}
<div class="panel">
<div class="panel-heading">Directories</div>
{% for served_directory in directories %}
<div class="panel-block">
<span class="panel-icon">
<i class="fas fa-folder fa-lg" aria-hidden="true"></i>
</span>
<a href="{% url 'browse' served_directory.id %}">
{{ served_directory.path }}
</a>
</div>
{% endfor %}
</div>
{% endblock content %}

View File

@@ -0,0 +1,15 @@
{% extends 'base.html' %}
{% block content %}
<div class="card">
<header class="card-header">
<p class="card-header-title">
{{ title }}
</p>
</header>
<div class="card-content">
<div class="content">
{{ message }}
</div>
</div>
</div>
{% endblock content %}

View File

@@ -3,5 +3,7 @@ from django.urls import path
from . import views from . import views
urlpatterns = [ urlpatterns = [
path('', views.index, name='index') path('', views.index, name='index'),
path('/<uuid:directory_id>/', views.browse, name='browse'),
path('/<uuid:directory_id>/<str:file>/', views.file, name='file')
] ]

View File

@@ -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): def index(request):
"""Index view for the simple-viewer project.""" """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)