Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Seatable thumbnail #1

Merged
merged 15 commits into from
Sep 10, 2020
145 changes: 145 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# 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/
pip-wheel-metadata/
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/

# 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
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.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/

#
.idea/
.vscode/

# log
logs/

# thumbnail
thumbnail/

# nohup
nohup.out

#
.DS_Store
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
# seatable-thumbnail-server

Thumbnail server for SeaTable

`uvicorn main:app --host 127.0.0.1 --port 8088`
35 changes: 35 additions & 0 deletions default
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
server {
if ($host = thumbnail.seatable.cn) {
return 301 https://$host$request_uri;
}

listen 80;
server_name thumbnail.seatable.cn;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个文件中的缩进不太对

return 404;
}


server {
server_name thumbnail.seatable.cn;

listen 443 ssl;
ssl_certificate /root/.acme.sh/thumbnail.seatable.cn/fullchain.cer;
ssl_certificate_key /root/.acme.sh/thumbnail.seatable.cn/thumbnail.seatable.cn.key;

access_log /var/log/nginx/thumbnail.access.log;
error_log /var/log/nginx/thumbnail.error.log;

# client_header_buffer_size 1k;
# large_client_header_buffers 4 4k/8k;

location / {
proxy_pass http://localhost:8088;
# proxy_pass http://unix:/var/run/thumbnail.sock;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}

}
102 changes: 102 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import logging

from seatable_thumbnail.serializers import ThumbnailSerializer
from seatable_thumbnail.permissions import ThumbnailPermission
from seatable_thumbnail.thumbnail import Thumbnail
from seatable_thumbnail.http_request import HTTPRequest
from seatable_thumbnail.http_response import gen_error_response, \
gen_text_response, gen_thumbnail_response, gen_cache_response

logger = logging.getLogger(__name__)


class App:
async def __call__(self, scope, receive, send):
"""
Docs: https://www.uvicorn.org/
"""
# request
request = HTTPRequest(**scope)
if request.method != 'GET':
response_start, response_body = gen_error_response(
405, 'Method %s not allowed.' % request.method)
await send(response_start)
await send(response_body)
return

# router
# ===== ping =====
if request.url in ('ping', 'ping/'):
response_start, response_body = gen_text_response('pong')
await send(response_start)
await send(response_body)
return

# ===== thumbnail =====
elif 'thumbnail/' == request.url[:10]:
# serializer
try:
serializer = ThumbnailSerializer(request)
thumbnail_info = serializer.thumbnail_info
except Exception as e:
logger.exception(e)
response_start, response_body = gen_error_response(
400, 'Request invalid.')
await send(response_start)
await send(response_body)
return

# permission
try:
permission = ThumbnailPermission(**thumbnail_info)
except Exception as e:
logger.exception(e)
response_start, response_body = gen_error_response(
403, 'Forbidden.')
await send(response_start)
await send(response_body)
return

# cache
try:
if_modified_since_list = request.headers.get('if-modified-since')
if if_modified_since_list:
if_modified_since = if_modified_since_list[0].encode('utf-8')
last_modified = thumbnail_info.get('last_modified')
if if_modified_since and last_modified \
and if_modified_since == last_modified:
response_start, response_body = gen_cache_response()
await send(response_start)
await send(response_body)
return
except Exception as e:
logger.exception(e)

# get or generate
try:
thumbnail = Thumbnail(**thumbnail_info)
body = thumbnail.body
last_modified = thumbnail.last_modified
# send
response_start, response_body = gen_thumbnail_response(body, last_modified)
await send(response_start)
await send(response_body)
return
except Exception as e:
logger.exception(e)
response_start, response_body = gen_error_response(
500, 'Generate failed.')
await send(response_start)
await send(response_body)
return

# ===== Not found =====
else:
response_start, response_body = gen_error_response(
404, 'Not Found.')
await send(response_start)
await send(response_body)
return


app = App()
9 changes: 9 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
uvicorn
pyjwt
pillow
pymysql
sqlalchemy
future # seaf-server

# psd_tools
# moviepy
3 changes: 3 additions & 0 deletions restart.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash

/usr/local/bin/supervisorctl -c /etc/supervisor/supervisord.conf reload
15 changes: 15 additions & 0 deletions seatable_thumbnail/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

import seatable_thumbnail.settings as settings

db_url = 'mysql+pymysql://%s:%s@%s:%s/%s?charset=utf8mb4' % \
(settings.MYSQL_USER, settings.MYSQL_PASSWORD,
settings.MYSQL_HOST, settings.MYSQL_PORT, settings.DATABASE_NAME)
db_kwargs = dict(pool_recycle=300, echo=False, echo_pool=False)

engine = create_engine(db_url, **db_kwargs)
Base = declarative_base()
Session = sessionmaker(bind=engine)
session = Session()
70 changes: 70 additions & 0 deletions seatable_thumbnail/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# content type
TEXT_CONTENT_TYPE = b'text/plain'
THUMBNAIL_CONTENT_TYPE = b'image/png'


# jwt
JWT_ALGORITHM = 'HS256'
JWT_VERIFY = True
JWT_LEEWAY = 0
JWT_AUDIENCE = None
JWT_ISSUER = None


# permission
PERMISSION_READ = 'r'
PERMISSION_READ_WRITE = 'rw'


# file type
EMPTY_BYTES = b''
THUMBNAIL_EXTENSION = 'png'
IMAGE_MODES = ('1', 'L', 'P', 'RGB', 'RGBA',)

TEXT = 'Text'
IMAGE = 'Image'
DOCUMENT = 'Document'
SVG = 'SVG'
PSD = 'PSD'
DRAW = 'Draw'
PDF = 'PDF'
MARKDOWN = 'Markdown'
VIDEO = 'Video'
AUDIO = 'Audio'
SPREADSHEET = 'SpreadSheet'
XMIND = 'XMind'
CDOC = 'cdoc'

PREVIEW_file_ext = {
IMAGE: ('gif', 'jpeg', 'jpg', 'png', 'ico', 'bmp', 'tif', 'tiff',),
DOCUMENT: ('doc', 'docx', 'ppt', 'pptx', 'odt', 'fodt', 'odp', 'fodp',),
SPREADSHEET: ('xls', 'xlsx', 'ods', 'fods',),
SVG: ('svg',),
PSD: ('psd',),
DRAW: ('draw',),
PDF: ('pdf', 'ai',),
MARKDOWN: ('markdown', 'md',),
VIDEO: ('mp4', 'ogv', 'webm', 'mov',),
AUDIO: ('mp3', 'oga', 'ogg',),
'3D': ('stl', 'obj',),
XMIND: ('xmind',),
CDOC: ('cdoc',),
TEXT: ('ac', 'am', 'bat', 'c', 'cc', 'cmake', 'cpp', 'cs', 'css', 'diff',
'el', 'h', 'html', 'htm', 'java', 'js', 'json', 'less', 'make',
'org', 'php', 'pl', 'properties', 'py', 'rb', 'scala', 'script',
'sh', 'sql', 'txt', 'text', 'tex', 'vi', 'vim', 'xhtml', 'xml',
'log', 'csv', 'groovy', 'rst', 'patch', 'go', 'yml',),
}


def gen_file_ext_type_map():
file_ext_type_map = {}

for file_type in list(PREVIEW_file_ext.keys()):
for file_ext in PREVIEW_file_ext.get(file_type):
file_ext_type_map[file_ext] = file_type

return file_ext_type_map


FILE_EXT_TYPE_MAP = gen_file_ext_type_map()
Loading