use env vars for config

This commit is contained in:
dave 2019-07-06 11:25:15 -07:00
parent 3660f4d9dc
commit 3bbe0f20ea
9 changed files with 81 additions and 33 deletions

View File

@ -10,3 +10,6 @@ styles/css/
styles/dist/ styles/dist/
styles/mincss/ styles/mincss/
testenv/ testenv/
source/
source_copy/
raws/

View File

@ -3,7 +3,7 @@ FROM ubuntu:bionic
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y wget software-properties-common && \ apt-get install -y wget software-properties-common && \
echo "deb https://deb.nodesource.com/node_10.x bionic main" | tee /etc/apt/sources.list.d/nodesource.list && \ echo "deb https://deb.nodesource.com/node_10.x bionic main" | tee /etc/apt/sources.list.d/nodesource.list && \
wget -O- https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - && \ wget -q -O- https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - && \
apt-get update && \ apt-get update && \
apt-get install -y nodejs apt-get install -y nodejs
@ -13,7 +13,7 @@ RUN cd /tmp/code && \
npm install && \ npm install && \
./node_modules/.bin/grunt ./node_modules/.bin/grunt
FROM ubuntu:bionic FROM ubuntu:disco
ADD . /tmp/code/ ADD . /tmp/code/
@ -24,7 +24,7 @@ RUN apt-get update && \
RUN pip3 install -U pip && \ RUN pip3 install -U pip && \
cd /tmp/code && \ cd /tmp/code && \
pip install -r requirements.txt && \ pip3 install -r requirements.txt && \
python3 setup.py install && \ python3 setup.py install && \
useradd --uid 1000 app useradd --uid 1000 app
@ -33,5 +33,6 @@ VOLUME /srv/cache
VOLUME /srv/db VOLUME /srv/db
USER app USER app
ENV CACHE_PATH=/tmp/cache
ENTRYPOINT ["photoappd", "--library", "/srv/library", "--database", "/srv/db/photos.db", "--cache", "/srv/cache"] ENTRYPOINT ["photoappd"]

View File

@ -42,17 +42,25 @@ Usage
After installation, run the server: After installation, run the server:
* `photoappd --port 8080 --library ./library --database ./photos.db --cache ./cache` * `photoappd --port 8080 --library file://./library --database sqlite:///photos.db --cache ./cache`
Arguments are as follows: Arguments are as follows:
* `--library ./library` - store the hosted image files under this directory * `--library file://./library` - file storage uri, in this case the relative path `./library`
* `--database ./photos.db` - store the SQLite database in this directory * `--database sqlite:///photos.db` - [Sqlalchemy](https://docs.sqlalchemy.org/en/13/core/engines.html) connection uri
* `--cache ./cache` - use this directory as a cache for things like thumbnails * `--cache ./cache` - use this directory as a cache for things like thumbnails
* `--port 8080` - listen on http on port 8080 * `--port 8080` - listen on http on port 8080
Supported library uri schemes are:
* file - relative (as above) or absolute file paths - `file:///srv/library`.
* minio - `minio://username:password@host/bucket_name/path/prefix`
Photolib is tested with [Minio](https://min.io/), but should work with S3. For the database, minio is tested with SQLite
and MySQL (using the `mysql+pymysql://` driver).
Next, the `photousers` command can be used to create a user account. Login information is necessary to see images marked Next, the `photousers` command can be used to create a user account. Login information is necessary to see images marked
as private or upload images. as private or upload images. You may want to run this before starting the server, but either order works.
* `photousers create -u dave -p mypassword` * `photousers create -u dave -p mypassword`
@ -112,13 +120,19 @@ Roadmap
- ~~Standardize on API ingest~~ done - ~~Standardize on API ingest~~ done
- ~~Display and retrieval of images from the abstracted image store~~ done - ~~Display and retrieval of images from the abstracted image store~~ done
- ~~Thumbnail gen~~ done - ~~Thumbnail gen~~ done
- Database support - ~~Database support~~
- ~~Get the web UI code (daemon.py) using the same db access method as the api~~ - ~~Get the web UI code (daemon.py) using the same db access method as the api~~
- Support any connection URI sqlalchemy is happy with - ~~Support any connection URI sqlalchemy is happy with~~
- Tune certain databases if their uri is detected (sqlite and threads lol) - ~~Tune certain databases if their uri is detected (sqlite and threads lol)~~
- ~~Cache~~ done - ~~Cache~~ done
- ~~Using the local fs seems fine?~~ done - ~~Using the local fs seems fine?~~ done
- Option to cherrypy sessions - Redis? - Migration path
- open database
- copy files table to memory
- recreate files table
- insert into the new table, with replaced paths, generating a list of files moves at the same time
- migrate files to the new storage according to the list
- Storage option for cherrypy sessions - Redis?
- Flesh out CLI: - Flesh out CLI:
- Config that is saved somewhere - Config that is saved somewhere
- Support additional fields on upload like title description tags etc - Support additional fields on upload like title description tags etc

View File

@ -214,6 +214,7 @@ def main():
e = future.exception() e = future.exception()
if e: if e:
results.append([set_fnames, "exception", repr(e)]) results.append([set_fnames, "exception", repr(e)])
numerrors += 1
else: else:
result = future.result() result = future.result()
if result[0] != "success": if result[0] != "success":

View File

@ -422,14 +422,26 @@ def main():
parser = argparse.ArgumentParser(description="Photod photo server") parser = argparse.ArgumentParser(description="Photod photo server")
parser.add_argument('-p', '--port', default=8080, type=int, help="tcp port to listen on") parser.add_argument('-p', '--port', help="tcp port to listen on",
parser.add_argument('-l', '--library', default="./library", help="library path") default=int(os.environ.get("PHOTOLIB_PORT", 8080)), type=int)
parser.add_argument('-c', '--cache', default="./cache", help="cache path") parser.add_argument('-l', '--library', default=os.environ.get("STORAGE_URL"), help="library path")
parser.add_argument('-s', '--database', default="./photos.db", help="path to persistent sqlite database") parser.add_argument('-c', '--cache', default=os.environ.get("CACHE_PATH"), help="cache path")
# https://docs.sqlalchemy.org/en/13/core/engines.html
parser.add_argument('-s', '--database', help="sqlalchemy database connection uri",
default=os.environ.get("DATABASE_URL")),
parser.add_argument('--debug', action="store_true", help="enable development options") parser.add_argument('--debug', action="store_true", help="enable development options")
args = parser.parse_args() args = parser.parse_args()
if not args.database:
parser.error("--database or DATABASE_URL is required")
if not args.library:
parser.error("--library or STORAGE_URL is required")
if not args.cache:
parser.error("--cache or CACHE_PATH is required")
logging.basicConfig(level=logging.INFO if args.debug else logging.WARNING, logging.basicConfig(level=logging.INFO if args.debug else logging.WARNING,
format="%(asctime)-15s %(levelname)-8s %(filename)s:%(lineno)d %(message)s") format="%(asctime)-15s %(levelname)-8s %(filename)s:%(lineno)d %(message)s")

View File

@ -2,17 +2,28 @@ import sqlalchemy
import cherrypy import cherrypy
from cherrypy.process import plugins from cherrypy.process import plugins
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.pool import StaticPool, AssertionPool, NullPool from sqlalchemy.pool import NullPool
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
Base = declarative_base() Base = declarative_base()
engine_specific_options = {"sqlite": dict(connect_args={'check_same_thread': False},
poolclass=NullPool,
pool_pre_ping=True),
"mysql": dict(pool_pre_ping=True)}
def get_engine_options(uri):
for engine_prefix, options in engine_specific_options.items():
if uri.startswith(engine_prefix):
return options
return {}
def get_db_engine(uri): def get_db_engine(uri):
# TODO handle more uris engine = sqlalchemy.create_engine(uri, **get_engine_options(uri))
engine = sqlalchemy.create_engine('sqlite:///{}'.format(uri),
connect_args={'check_same_thread': False}, poolclass=NullPool, pool_pre_ping=True)
Base.metadata.create_all(engine) Base.metadata.create_all(engine)
return engine return engine

View File

@ -50,7 +50,7 @@ class PhotoSet(Base):
__tablename__ = 'photos' __tablename__ = 'photos'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
uuid = Column(Unicode, unique=True, default=lambda: str(uuid.uuid4())) uuid = Column(Unicode(length=36), unique=True, default=lambda: str(uuid.uuid4()))
date = Column(DateTime) date = Column(DateTime)
date_real = Column(DateTime) date_real = Column(DateTime)
date_offset = Column(Integer, default=0) # minutes date_offset = Column(Integer, default=0) # minutes
@ -60,9 +60,9 @@ class PhotoSet(Base):
files = relationship("Photo", back_populates="set") files = relationship("Photo", back_populates="set")
tags = relationship("TagItem", back_populates="set") tags = relationship("TagItem", back_populates="set")
title = Column(String) title = Column(String(length=128))
description = Column(String) description = Column(String(length=1024))
slug = Column(String) slug = Column(String(length=128))
status = Column(Enum(PhotoStatus), default=PhotoStatus.private) status = Column(Enum(PhotoStatus), default=PhotoStatus.private)
@ -83,7 +83,7 @@ class Photo(Base):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
set_id = Column(Integer, ForeignKey("photos.id")) set_id = Column(Integer, ForeignKey("photos.id"))
uuid = Column(Unicode, unique=True, default=genuuid) uuid = Column(Unicode(length=36), unique=True, default=genuuid)
set = relationship("PhotoSet", back_populates="files", foreign_keys=[set_id]) set = relationship("PhotoSet", back_populates="files", foreign_keys=[set_id])
@ -92,8 +92,8 @@ class Photo(Base):
height = Column(Integer) height = Column(Integer)
orientation = Column(Integer, default=0) orientation = Column(Integer, default=0)
hash = Column(String(length=64), unique=True) hash = Column(String(length=64), unique=True)
path = Column(Unicode) path = Column(Unicode(length=64))
format = Column(String(length=64)) # TODO how long can a mime string be format = Column(String(length=32)) # TODO how long can a mime string be
fname = Column(String(length=64)) # seems generous enough fname = Column(String(length=64)) # seems generous enough
def to_json(self): def to_json(self):
@ -107,18 +107,18 @@ class Tag(Base):
__tablename__ = 'tags' __tablename__ = 'tags'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
uuid = Column(Unicode, unique=True, default=lambda: str(uuid.uuid4())) uuid = Column(Unicode(length=36), unique=True, default=lambda: str(uuid.uuid4()))
created = Column(DateTime, default=lambda: datetime.now()) created = Column(DateTime, default=lambda: datetime.now())
modified = Column(DateTime, default=lambda: datetime.now()) modified = Column(DateTime, default=lambda: datetime.now())
is_album = Column(Boolean, default=False) is_album = Column(Boolean, default=False)
# slug-like short name such as "iomtrip" # slug-like short name such as "iomtrip"
name = Column(String, unique=True) name = Column(String(length=32), unique=True)
# longer human-format title like "Isle of Man trip" # longer human-format title like "Isle of Man trip"
title = Column(String) title = Column(String(length=32))
# url slug like "isle-of-man-trip" # url slug like "isle-of-man-trip"
slug = Column(String, unique=True) slug = Column(String(length=32), unique=True)
# fulltext description # fulltext description
description = Column(String) description = Column(String(length=256))
entries = relationship("TagItem", back_populates="tag") entries = relationship("TagItem", back_populates="tag")

View File

@ -1,3 +1,4 @@
import os
import argparse import argparse
from photoapp.types import User from photoapp.types import User
from photoapp.common import pwhash from photoapp.common import pwhash
@ -25,7 +26,8 @@ def delete_user(s, username):
def main(): def main():
parser = argparse.ArgumentParser(description="User manipulation tool") parser = argparse.ArgumentParser(description="User manipulation tool")
parser.add_argument("-d", "--database", help="database uri") parser.add_argument('-s', '--database', help="sqlalchemy database connection uri",
default=os.environ.get("DATABASE_URL")),
p_mode = parser.add_subparsers(dest='action', help='action to take') p_mode = parser.add_subparsers(dest='action', help='action to take')
@ -40,6 +42,9 @@ def main():
args = parser.parse_args() args = parser.parse_args()
if not args.database:
parser.error("--database or DATABASE_URL is required")
session = get_db_session(args.database)() session = get_db_session(args.database)()
if args.action == "create": if args.action == "create":

View File

@ -15,6 +15,7 @@ MarkupSafe==1.0
more-itertools==4.3.0 more-itertools==4.3.0
Pillow==5.2.0 Pillow==5.2.0
portend==2.3 portend==2.3
PyMySQL==0.9.3
python-dateutil==2.8.0 python-dateutil==2.8.0
python-magic==0.4.15 python-magic==0.4.15
pytz==2018.5 pytz==2018.5