use env vars for config
This commit is contained in:
parent
3660f4d9dc
commit
3bbe0f20ea
|
@ -10,3 +10,6 @@ styles/css/
|
||||||
styles/dist/
|
styles/dist/
|
||||||
styles/mincss/
|
styles/mincss/
|
||||||
testenv/
|
testenv/
|
||||||
|
source/
|
||||||
|
source_copy/
|
||||||
|
raws/
|
|
@ -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"]
|
||||||
|
|
30
README.md
30
README.md
|
@ -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
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue