Use new worms version.

This commit is contained in:
voussoir 2022-07-15 23:00:07 -07:00
parent a436dafa9c
commit 0e00a1e981
No known key found for this signature in database
GPG key ID: 5F7554F8C26DACCB
10 changed files with 388 additions and 361 deletions

View file

@ -42,19 +42,8 @@ ffmpeg = _load_ffmpeg()
# Database ######################################################################################### # Database #########################################################################################
DATABASE_VERSION = 20 DATABASE_VERSION = 20
DB_VERSION_PRAGMA = f'''
PRAGMA user_version = {DATABASE_VERSION};
'''
DB_PRAGMAS = f'''
PRAGMA cache_size = 10000;
PRAGMA foreign_keys = ON;
'''
DB_INIT = f''' DB_INIT = f'''
BEGIN;
{DB_PRAGMAS}
{DB_VERSION_PRAGMA}
---------------------------------------------------------------------------------------------------- ----------------------------------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS albums( CREATE TABLE IF NOT EXISTS albums(
id TEXT PRIMARY KEY NOT NULL, id TEXT PRIMARY KEY NOT NULL,
@ -191,8 +180,6 @@ CREATE TABLE IF NOT EXISTS tag_synonyms(
mastername TEXT NOT NULL mastername TEXT NOT NULL
); );
CREATE INDEX IF NOT EXISTS index_tag_synonyms_name on tag_synonyms(name); CREATE INDEX IF NOT EXISTS index_tag_synonyms_name on tag_synonyms(name);
----------------------------------------------------------------------------------------------------
COMMIT;
''' '''
SQL_COLUMNS = sqlhelpers.extract_table_column_map(DB_INIT) SQL_COLUMNS = sqlhelpers.extract_table_column_map(DB_INIT)

View file

@ -329,7 +329,7 @@ class Album(ObjectBase, GroupableMixin):
self.photodb.insert(table='album_associated_directories', data=data) self.photodb.insert(table='album_associated_directories', data=data)
@decorators.required_feature('album.edit') @decorators.required_feature('album.edit')
@worms.transaction @worms.atomic
def add_associated_directory(self, path) -> None: def add_associated_directory(self, path) -> None:
''' '''
Add a directory from which this album will pull files during rescans. Add a directory from which this album will pull files during rescans.
@ -341,7 +341,7 @@ class Album(ObjectBase, GroupableMixin):
self._add_associated_directory(path) self._add_associated_directory(path)
@decorators.required_feature('album.edit') @decorators.required_feature('album.edit')
@worms.transaction @worms.atomic
def add_associated_directories(self, paths) -> None: def add_associated_directories(self, paths) -> None:
''' '''
Add multiple associated directories. Add multiple associated directories.
@ -352,7 +352,7 @@ class Album(ObjectBase, GroupableMixin):
self._add_associated_directory(path) self._add_associated_directory(path)
@decorators.required_feature('album.edit') @decorators.required_feature('album.edit')
@worms.transaction @worms.atomic
def add_child(self, member): def add_child(self, member):
''' '''
Raises exceptions.CantGroupSelf if member is self. Raises exceptions.CantGroupSelf if member is self.
@ -362,7 +362,7 @@ class Album(ObjectBase, GroupableMixin):
return super().add_child(member) return super().add_child(member)
@decorators.required_feature('album.edit') @decorators.required_feature('album.edit')
@worms.transaction @worms.atomic
def add_children(self, *args, **kwargs): def add_children(self, *args, **kwargs):
return super().add_children(*args, **kwargs) return super().add_children(*args, **kwargs)
@ -372,7 +372,7 @@ class Album(ObjectBase, GroupableMixin):
self.photodb.insert(table='album_photo_rel', data=data) self.photodb.insert(table='album_photo_rel', data=data)
@decorators.required_feature('album.edit') @decorators.required_feature('album.edit')
@worms.transaction @worms.atomic
def add_photo(self, photo) -> None: def add_photo(self, photo) -> None:
if self.has_photo(photo): if self.has_photo(photo):
return return
@ -380,7 +380,7 @@ class Album(ObjectBase, GroupableMixin):
self._add_photo(photo) self._add_photo(photo)
@decorators.required_feature('album.edit') @decorators.required_feature('album.edit')
@worms.transaction @worms.atomic
def add_photos(self, photos) -> None: def add_photos(self, photos) -> None:
existing_photos = set(self.get_photos()) existing_photos = set(self.get_photos())
photos = set(photos) photos = set(photos)
@ -393,7 +393,7 @@ class Album(ObjectBase, GroupableMixin):
self._add_photo(photo) self._add_photo(photo)
# Photo.add_tag already has @required_feature # Photo.add_tag already has @required_feature
@worms.transaction @worms.atomic
def add_tag_to_all(self, tag, *, nested_children=True) -> None: def add_tag_to_all(self, tag, *, nested_children=True) -> None:
''' '''
Add this tag to every photo in the album. Saves you from having to Add this tag to every photo in the album. Saves you from having to
@ -413,7 +413,7 @@ class Album(ObjectBase, GroupableMixin):
photo.add_tag(tag) photo.add_tag(tag)
@decorators.required_feature('album.edit') @decorators.required_feature('album.edit')
@worms.transaction @worms.atomic
def delete(self, *, delete_children=False) -> None: def delete(self, *, delete_children=False) -> None:
log.info('Deleting %s.', self) log.info('Deleting %s.', self)
GroupableMixin.delete(self, delete_children=delete_children) GroupableMixin.delete(self, delete_children=delete_children)
@ -431,7 +431,7 @@ class Album(ObjectBase, GroupableMixin):
return self.id return self.id
@decorators.required_feature('album.edit') @decorators.required_feature('album.edit')
@worms.transaction @worms.atomic
def edit(self, title=None, description=None) -> None: def edit(self, title=None, description=None) -> None:
''' '''
Change the title or description. Leave None to keep current value. Change the title or description. Leave None to keep current value.
@ -546,12 +546,12 @@ class Album(ObjectBase, GroupableMixin):
return j return j
@decorators.required_feature('album.edit') @decorators.required_feature('album.edit')
@worms.transaction @worms.atomic
def remove_child(self, *args, **kwargs): def remove_child(self, *args, **kwargs):
return super().remove_child(*args, **kwargs) return super().remove_child(*args, **kwargs)
@decorators.required_feature('album.edit') @decorators.required_feature('album.edit')
@worms.transaction @worms.atomic
def remove_children(self, *args, **kwargs): def remove_children(self, *args, **kwargs):
return super().remove_children(*args, **kwargs) return super().remove_children(*args, **kwargs)
@ -561,12 +561,12 @@ class Album(ObjectBase, GroupableMixin):
self.photodb.delete(table='album_photo_rel', pairs=pairs) self.photodb.delete(table='album_photo_rel', pairs=pairs)
@decorators.required_feature('album.edit') @decorators.required_feature('album.edit')
@worms.transaction @worms.atomic
def remove_photo(self, photo) -> None: def remove_photo(self, photo) -> None:
self._remove_photo(photo) self._remove_photo(photo)
@decorators.required_feature('album.edit') @decorators.required_feature('album.edit')
@worms.transaction @worms.atomic
def remove_photos(self, photos) -> None: def remove_photos(self, photos) -> None:
existing_photos = set(self.get_photos()) existing_photos = set(self.get_photos())
photos = set(photos) photos = set(photos)
@ -579,7 +579,7 @@ class Album(ObjectBase, GroupableMixin):
self._remove_photo(photo) self._remove_photo(photo)
@decorators.required_feature('album.edit') @decorators.required_feature('album.edit')
@worms.transaction @worms.atomic
def set_thumbnail_photo(self, photo) -> None: def set_thumbnail_photo(self, photo) -> None:
''' '''
Raises TypeError if photo is not a Photo. Raises TypeError if photo is not a Photo.
@ -747,7 +747,7 @@ class Bookmark(ObjectBase):
self.photodb.caches[Bookmark].remove(self.id) self.photodb.caches[Bookmark].remove(self.id)
@decorators.required_feature('bookmark.edit') @decorators.required_feature('bookmark.edit')
@worms.transaction @worms.atomic
def delete(self) -> None: def delete(self) -> None:
self.photodb.delete(table=Bookmark, pairs={'id': self.id}) self.photodb.delete(table=Bookmark, pairs={'id': self.id})
self._uncache() self._uncache()
@ -761,7 +761,7 @@ class Bookmark(ObjectBase):
return self.id return self.id
@decorators.required_feature('bookmark.edit') @decorators.required_feature('bookmark.edit')
@worms.transaction @worms.atomic
def edit(self, title=None, url=None) -> None: def edit(self, title=None, url=None) -> None:
''' '''
Change the title or URL. Leave None to keep current. Change the title or URL. Leave None to keep current.
@ -894,7 +894,7 @@ class Photo(ObjectBase):
# Will add -> Tag when forward references are supported by Python. # Will add -> Tag when forward references are supported by Python.
@decorators.required_feature('photo.add_remove_tag') @decorators.required_feature('photo.add_remove_tag')
@worms.transaction @worms.atomic
def add_tag(self, tag): def add_tag(self, tag):
tag = self.photodb.get_tag(name=tag) tag = self.photodb.get_tag(name=tag)
@ -948,7 +948,7 @@ class Photo(ObjectBase):
return '??? b' return '??? b'
# Photo.add_tag already has @required_feature add_remove_tag # Photo.add_tag already has @required_feature add_remove_tag
@worms.transaction @worms.atomic
def copy_tags(self, other_photo) -> None: def copy_tags(self, other_photo) -> None:
''' '''
Take all of the tags owned by other_photo and apply them to this photo. Take all of the tags owned by other_photo and apply them to this photo.
@ -957,7 +957,7 @@ class Photo(ObjectBase):
self.add_tag(tag) self.add_tag(tag)
@decorators.required_feature('photo.edit') @decorators.required_feature('photo.edit')
@worms.transaction @worms.atomic
def delete(self, *, delete_file=False) -> None: def delete(self, *, delete_file=False) -> None:
''' '''
Delete the Photo and its relation to any tags and albums. Delete the Photo and its relation to any tags and albums.
@ -995,7 +995,7 @@ class Photo(ObjectBase):
return hms.seconds_to_hms(self.duration) return hms.seconds_to_hms(self.duration)
@decorators.required_feature('photo.generate_thumbnail') @decorators.required_feature('photo.generate_thumbnail')
@worms.transaction @worms.atomic
def generate_thumbnail(self, **special) -> pathclass.Path: def generate_thumbnail(self, **special) -> pathclass.Path:
''' '''
special: special:
@ -1144,7 +1144,7 @@ class Photo(ObjectBase):
return hopeful_filepath return hopeful_filepath
# Photo.rename_file already has @required_feature # Photo.rename_file already has @required_feature
@worms.transaction @worms.atomic
def move_file(self, directory) -> None: def move_file(self, directory) -> None:
directory = pathclass.Path(directory) directory = pathclass.Path(directory)
directory.assert_is_directory() directory.assert_is_directory()
@ -1195,7 +1195,7 @@ class Photo(ObjectBase):
self.duration = probe.audio.duration self.duration = probe.audio.duration
@decorators.required_feature('photo.reload_metadata') @decorators.required_feature('photo.reload_metadata')
@worms.transaction @worms.atomic
def reload_metadata(self, hash_kwargs=None) -> None: def reload_metadata(self, hash_kwargs=None) -> None:
''' '''
Load the file's height, width, etc as appropriate for this type of file. Load the file's height, width, etc as appropriate for this type of file.
@ -1252,7 +1252,7 @@ class Photo(ObjectBase):
self._uncache() self._uncache()
@decorators.required_feature('photo.edit') @decorators.required_feature('photo.edit')
@worms.transaction @worms.atomic
def relocate(self, new_filepath) -> None: def relocate(self, new_filepath) -> None:
''' '''
Point the Photo object to a different filepath. Point the Photo object to a different filepath.
@ -1287,7 +1287,7 @@ class Photo(ObjectBase):
self._uncache() self._uncache()
@decorators.required_feature('photo.add_remove_tag') @decorators.required_feature('photo.add_remove_tag')
@worms.transaction @worms.atomic
def remove_tag(self, tag) -> None: def remove_tag(self, tag) -> None:
tag = self.photodb.get_tag(name=tag) tag = self.photodb.get_tag(name=tag)
@ -1305,7 +1305,7 @@ class Photo(ObjectBase):
self.photodb.update(table=Photo, pairs=data, where_key='id') self.photodb.update(table=Photo, pairs=data, where_key='id')
@decorators.required_feature('photo.add_remove_tag') @decorators.required_feature('photo.add_remove_tag')
@worms.transaction @worms.atomic
def remove_tags(self, tags) -> None: def remove_tags(self, tags) -> None:
tags = [self.photodb.get_tag(name=tag) for tag in tags] tags = [self.photodb.get_tag(name=tag) for tag in tags]
@ -1324,7 +1324,7 @@ class Photo(ObjectBase):
self.photodb.update(table=Photo, pairs=data, where_key='id') self.photodb.update(table=Photo, pairs=data, where_key='id')
@decorators.required_feature('photo.edit') @decorators.required_feature('photo.edit')
@worms.transaction @worms.atomic
def rename_file(self, new_filename, *, move=False) -> None: def rename_file(self, new_filename, *, move=False) -> None:
''' '''
Rename the file on the disk as well as in the database. Rename the file on the disk as well as in the database.
@ -1411,7 +1411,7 @@ class Photo(ObjectBase):
self.__reinit__() self.__reinit__()
@decorators.required_feature('photo.edit') @decorators.required_feature('photo.edit')
@worms.transaction @worms.atomic
def set_override_filename(self, new_filename) -> None: def set_override_filename(self, new_filename) -> None:
new_filename = self.normalize_override_filename(new_filename) new_filename = self.normalize_override_filename(new_filename)
@ -1425,7 +1425,7 @@ class Photo(ObjectBase):
self.__reinit__() self.__reinit__()
@decorators.required_feature('photo.edit') @decorators.required_feature('photo.edit')
@worms.transaction @worms.atomic
def set_searchhidden(self, searchhidden) -> None: def set_searchhidden(self, searchhidden) -> None:
data = { data = {
'id': self.id, 'id': self.id,
@ -1537,7 +1537,7 @@ class Tag(ObjectBase, GroupableMixin):
photo.remove_tags(ancestors) photo.remove_tags(ancestors)
@decorators.required_feature('tag.edit') @decorators.required_feature('tag.edit')
@worms.transaction @worms.atomic
def add_child(self, member): def add_child(self, member):
''' '''
Raises exceptions.CantGroupSelf if member is self. Raises exceptions.CantGroupSelf if member is self.
@ -1552,7 +1552,7 @@ class Tag(ObjectBase, GroupableMixin):
return ret return ret
@decorators.required_feature('tag.edit') @decorators.required_feature('tag.edit')
@worms.transaction @worms.atomic
def add_children(self, members): def add_children(self, members):
ret = super().add_children(members) ret = super().add_children(members)
if ret is BAIL: if ret is BAIL:
@ -1562,7 +1562,7 @@ class Tag(ObjectBase, GroupableMixin):
return ret return ret
@decorators.required_feature('tag.edit') @decorators.required_feature('tag.edit')
@worms.transaction @worms.atomic
def add_synonym(self, synname) -> str: def add_synonym(self, synname) -> str:
''' '''
Raises any exceptions from photodb.normalize_tagname. Raises any exceptions from photodb.normalize_tagname.
@ -1594,7 +1594,7 @@ class Tag(ObjectBase, GroupableMixin):
return synname return synname
@decorators.required_feature('tag.edit') @decorators.required_feature('tag.edit')
@worms.transaction @worms.atomic
def convert_to_synonym(self, mastertag) -> None: def convert_to_synonym(self, mastertag) -> None:
''' '''
Convert this tag into a synonym for a different tag. Convert this tag into a synonym for a different tag.
@ -1656,7 +1656,7 @@ class Tag(ObjectBase, GroupableMixin):
mastertag.add_synonym(self.name) mastertag.add_synonym(self.name)
@decorators.required_feature('tag.edit') @decorators.required_feature('tag.edit')
@worms.transaction @worms.atomic
def delete(self, *, delete_children=False) -> None: def delete(self, *, delete_children=False) -> None:
log.info('Deleting %s.', self) log.info('Deleting %s.', self)
super().delete(delete_children=delete_children) super().delete(delete_children=delete_children)
@ -1668,7 +1668,7 @@ class Tag(ObjectBase, GroupableMixin):
self.deleted = True self.deleted = True
@decorators.required_feature('tag.edit') @decorators.required_feature('tag.edit')
@worms.transaction @worms.atomic
def edit(self, description=None) -> None: def edit(self, description=None) -> None:
''' '''
Change the description. Leave None to keep current value. Change the description. Leave None to keep current value.
@ -1718,7 +1718,7 @@ class Tag(ObjectBase, GroupableMixin):
return j return j
@decorators.required_feature('tag.edit') @decorators.required_feature('tag.edit')
@worms.transaction @worms.atomic
def remove_child(self, *args, **kwargs): def remove_child(self, *args, **kwargs):
ret = super().remove_child(*args, **kwargs) ret = super().remove_child(*args, **kwargs)
if ret is BAIL: if ret is BAIL:
@ -1728,7 +1728,7 @@ class Tag(ObjectBase, GroupableMixin):
return ret return ret
@decorators.required_feature('tag.edit') @decorators.required_feature('tag.edit')
@worms.transaction @worms.atomic
def remove_children(self, *args, **kwargs): def remove_children(self, *args, **kwargs):
ret = super().remove_children(*args, **kwargs) ret = super().remove_children(*args, **kwargs)
if ret is BAIL: if ret is BAIL:
@ -1738,7 +1738,7 @@ class Tag(ObjectBase, GroupableMixin):
return ret return ret
@decorators.required_feature('tag.edit') @decorators.required_feature('tag.edit')
@worms.transaction @worms.atomic
def remove_synonym(self, synname) -> str: def remove_synonym(self, synname) -> str:
''' '''
Delete a synonym. Delete a synonym.
@ -1770,7 +1770,7 @@ class Tag(ObjectBase, GroupableMixin):
return synname return synname
@decorators.required_feature('tag.edit') @decorators.required_feature('tag.edit')
@worms.transaction @worms.atomic
def rename(self, new_name, *, apply_to_synonyms=True) -> None: def rename(self, new_name, *, apply_to_synonyms=True) -> None:
''' '''
Rename the tag. Does not affect its relation to Photos or tag groups. Rename the tag. Does not affect its relation to Photos or tag groups.
@ -1860,7 +1860,7 @@ class User(ObjectBase):
self.photodb.caches[User].remove(self.id) self.photodb.caches[User].remove(self.id)
@decorators.required_feature('user.edit') @decorators.required_feature('user.edit')
@worms.transaction @worms.atomic
def delete(self, *, disown_authored_things) -> None: def delete(self, *, disown_authored_things) -> None:
''' '''
If disown_authored_things is True then all of this user's albums, If disown_authored_things is True then all of this user's albums,
@ -1978,7 +1978,7 @@ class User(ObjectBase):
return j return j
@decorators.required_feature('user.edit') @decorators.required_feature('user.edit')
@worms.transaction @worms.atomic
def set_display_name(self, display_name) -> None: def set_display_name(self, display_name) -> None:
display_name = self.normalize_display_name( display_name = self.normalize_display_name(
display_name, display_name,
@ -1993,7 +1993,7 @@ class User(ObjectBase):
self._display_name = display_name self._display_name = display_name
@decorators.required_feature('user.edit') @decorators.required_feature('user.edit')
@worms.transaction @worms.atomic
def set_password(self, password) -> None: def set_password(self, password) -> None:
if not isinstance(password, bytes): if not isinstance(password, bytes):
password = password.encode('utf-8') password = password.encode('utf-8')

View file

@ -9,6 +9,14 @@ import time
import types import types
import typing import typing
from . import constants
from . import decorators
from . import exceptions
from . import helpers
from . import objects
from . import searchhelpers
from . import tag_export
from voussoirkit import cacheclass from voussoirkit import cacheclass
from voussoirkit import configlayers from voussoirkit import configlayers
from voussoirkit import expressionmatch from voussoirkit import expressionmatch
@ -22,13 +30,6 @@ from voussoirkit import worms
log = vlogging.getLogger(__name__) log = vlogging.getLogger(__name__)
from . import constants
from . import decorators
from . import exceptions
from . import helpers
from . import objects
from . import searchhelpers
from . import tag_export
#################################################################################################### ####################################################################################################
@ -85,7 +86,7 @@ class PDBAlbumMixin:
return self.get_root_objects(objects.Album) return self.get_root_objects(objects.Album)
@decorators.required_feature('album.new') @decorators.required_feature('album.new')
@worms.transaction @worms.atomic
def new_album( def new_album(
self, self,
title=None, title=None,
@ -130,7 +131,7 @@ class PDBAlbumMixin:
return album return album
@worms.transaction @worms.atomic
def purge_deleted_associated_directories(self, albums=None) -> typing.Iterable[pathclass.Path]: def purge_deleted_associated_directories(self, albums=None) -> typing.Iterable[pathclass.Path]:
query = 'SELECT DISTINCT directory FROM album_associated_directories' query = 'SELECT DISTINCT directory FROM album_associated_directories'
directories = self.select_column(query) directories = self.select_column(query)
@ -148,7 +149,7 @@ class PDBAlbumMixin:
self.execute(query) self.execute(query)
yield from directories yield from directories
@worms.transaction @worms.atomic
def purge_empty_albums(self, albums=None) -> typing.Iterable[objects.Album]: def purge_empty_albums(self, albums=None) -> typing.Iterable[objects.Album]:
if albums is None: if albums is None:
to_check = set(self.get_albums()) to_check = set(self.get_albums())
@ -188,7 +189,7 @@ class PDBBookmarkMixin:
return self.get_objects_by_sql(objects.Bookmark, query, bindings) return self.get_objects_by_sql(objects.Bookmark, query, bindings)
@decorators.required_feature('bookmark.new') @decorators.required_feature('bookmark.new')
@worms.transaction @worms.atomic
def new_bookmark(self, url, title=None, *, author=None) -> objects.Bookmark: def new_bookmark(self, url, title=None, *, author=None) -> objects.Bookmark:
# These might raise exceptions. # These might raise exceptions.
title = objects.Bookmark.normalize_title(title) title = objects.Bookmark.normalize_title(title)
@ -305,7 +306,7 @@ class PDBPhotoMixin:
return self.get_objects_by_sql(objects.Photo, query, bindings) return self.get_objects_by_sql(objects.Photo, query, bindings)
@decorators.required_feature('photo.new') @decorators.required_feature('photo.new')
@worms.transaction @worms.atomic
def new_photo( def new_photo(
self, self,
filepath, filepath,
@ -390,7 +391,7 @@ class PDBPhotoMixin:
return photo return photo
@worms.transaction @worms.atomic
def purge_deleted_files(self, photos=None) -> typing.Iterable[objects.Photo]: def purge_deleted_files(self, photos=None) -> typing.Iterable[objects.Photo]:
''' '''
Delete Photos whose corresponding file on disk is missing. Delete Photos whose corresponding file on disk is missing.
@ -956,7 +957,7 @@ class PDBTagMixin:
return self.get_objects_by_sql(objects.Tag, query, bindings) return self.get_objects_by_sql(objects.Tag, query, bindings)
@decorators.required_feature('tag.new') @decorators.required_feature('tag.new')
@worms.transaction @worms.atomic
def new_tag(self, tagname, description=None, *, author=None) -> objects.Tag: def new_tag(self, tagname, description=None, *, author=None) -> objects.Tag:
''' '''
Register a new tag and return the Tag object. Register a new tag and return the Tag object.
@ -1138,7 +1139,7 @@ class PDBUserMixin:
return user return user
@decorators.required_feature('user.new') @decorators.required_feature('user.new')
@worms.transaction @worms.atomic
def new_user(self, username, password, *, display_name=None) -> objects.User: def new_user(self, username, password, *, display_name=None) -> objects.User:
# These might raise exceptions. # These might raise exceptions.
self.assert_valid_username(username) self.assert_valid_username(username)
@ -1177,7 +1178,7 @@ class PDBUtilMixin:
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@worms.transaction @worms.atomic
def digest_directory( def digest_directory(
self, self,
directory, directory,
@ -1436,7 +1437,7 @@ class PDBUtilMixin:
if yield_albums: if yield_albums:
yield from current_albums yield from current_albums
@worms.transaction @worms.atomic
def easybake(self, ebstring, author=None): def easybake(self, ebstring, author=None):
''' '''
Easily create tags, groups, and synonyms with a string like Easily create tags, groups, and synonyms with a string like
@ -1570,7 +1571,7 @@ class PhotoDB(
Compare database's user_version against constants.DATABASE_VERSION, Compare database's user_version against constants.DATABASE_VERSION,
raising exceptions.DatabaseOutOfDate if not correct. raising exceptions.DatabaseOutOfDate if not correct.
''' '''
existing = self.execute('PRAGMA user_version').fetchone()[0] existing = self.pragma_read('user_version')
if existing != constants.DATABASE_VERSION: if existing != constants.DATABASE_VERSION:
raise exceptions.DatabaseOutOfDate( raise exceptions.DatabaseOutOfDate(
existing=existing, existing=existing,
@ -1580,8 +1581,10 @@ class PhotoDB(
def _first_time_setup(self): def _first_time_setup(self):
log.info('Running first-time database setup.') log.info('Running first-time database setup.')
self.executescript(constants.DB_INIT) with self.transaction:
self.commit() self._load_pragmas()
self.pragma_write('user_version', constants.DATABASE_VERSION)
self.executescript(constants.DB_INIT)
def _init_caches(self): def _init_caches(self):
self.caches = { self.caches = {
@ -1600,8 +1603,8 @@ class PhotoDB(
def _init_sql(self, create, skip_version_check): def _init_sql(self, create, skip_version_check):
if self.ephemeral: if self.ephemeral:
existing_database = False existing_database = False
self.sql = sqlite3.connect(':memory:') self.sql_read = self._make_sqlite_read_connection(':memory:')
self.sql.row_factory = sqlite3.Row self.sql_write = self._make_sqlite_write_connection(':memory:')
self._first_time_setup() self._first_time_setup()
return return
@ -1613,21 +1616,21 @@ class PhotoDB(
raise FileNotFoundError(msg) raise FileNotFoundError(msg)
self.data_directory.makedirs(exist_ok=True) self.data_directory.makedirs(exist_ok=True)
log.debug('Connecting to sqlite file "%s".', self.database_filepath.absolute_path) self.sql_read = self._make_sqlite_read_connection(self.database_filepath)
self.sql = sqlite3.connect(self.database_filepath.absolute_path) self.sql_write = self._make_sqlite_write_connection(self.database_filepath)
self.sql.row_factory = sqlite3.Row
if existing_database: if existing_database:
if not skip_version_check: if not skip_version_check:
self._check_version() self._check_version()
self._load_pragmas() with self.transaction:
self._load_pragmas()
else: else:
self._first_time_setup() self._first_time_setup()
def _load_pragmas(self): def _load_pragmas(self):
log.debug('Reloading pragmas.') log.debug('Reloading pragmas.')
self.executescript(constants.DB_PRAGMAS) self.pragma_write('cache_size', 10000)
self.commit() self.pragma_write('foreign_keys', 'on')
# Will add -> PhotoDB when forward references are supported # Will add -> PhotoDB when forward references are supported
@classmethod @classmethod

View file

@ -65,7 +65,14 @@ def get_photos_by_glob(pattern):
if pattern == '**': if pattern == '**':
return search_in_cwd(yield_photos=True, yield_albums=False) return search_in_cwd(yield_photos=True, yield_albums=False)
for file in pathclass.glob_files(pattern): as_path = pathclass.Path(pattern)
if as_path.is_directory:
files = as_path.listdir_files()
else:
files = pathclass.glob_files(pattern)
for file in files:
try: try:
photo = photodb.get_photo_by_path(file) photo = photodb.get_photo_by_path(file)
yield photo yield photo
@ -76,7 +83,7 @@ def get_photos_by_globs(patterns):
for pattern in patterns: for pattern in patterns:
yield from get_photos_by_glob(pattern) yield from get_photos_by_glob(pattern)
def get_photos_from_args(args): def get_photos_from_args(args, fallback_search_in_cwd=False):
load_photodb() load_photodb()
photos = [] photos = []
@ -92,6 +99,9 @@ def get_photos_from_args(args):
if args.photo_search_args: if args.photo_search_args:
photos.extend(search_by_argparse(args.photo_search_args, yield_photos=True)) photos.extend(search_by_argparse(args.photo_search_args, yield_photos=True))
if (not photos) and fallback_search_in_cwd:
photos.extend(search_in_cwd(yield_photos=True, yield_albums=False))
return photos return photos
def get_albums_from_args(args): def get_albums_from_args(args):
@ -158,18 +168,20 @@ def add_remove_tag_argparse(args, action):
need_commit = False need_commit = False
for photo in photos: with photodb.transaction:
if action == 'add': for photo in photos:
photo.add_tag(tag) if action == 'add':
elif action == 'remove': photo.add_tag(tag)
photo.remove_tag(tag) elif action == 'remove':
need_commit = True photo.remove_tag(tag)
need_commit = True
if not need_commit: if not need_commit:
return 0 return 0
if args.autoyes or interactive.getpermission('Commit?'): if not (args.autoyes or interactive.getpermission('Commit?')):
photodb.commit() photodb.rollback()
return 1
return 0 return 0
@ -178,15 +190,18 @@ def delete_albums_argparse(args):
need_commit = False need_commit = False
albums = get_albums_from_args(args) albums = get_albums_from_args(args)
for album in albums:
album.delete()
need_commit = True
if not need_commit: with photodb.transaction:
return 0 for album in albums:
album.delete()
need_commit = True
if args.autoyes or interactive.getpermission('Commit?'): if not need_commit:
photodb.commit() return 0
if not (args.autoyes or interactive.getpermission('Commit?')):
photodb.rollback()
return 1
return 0 return 0
@ -195,15 +210,18 @@ def delete_photos_argparse(args):
need_commit = False need_commit = False
photos = get_photos_from_args(args) photos = get_photos_from_args(args)
for photo in photos:
photo.delete(delete_file=args.delete_file)
need_commit = True
if not need_commit: with photodb.transaction:
return 0 for photo in photos:
photo.delete(delete_file=args.delete_file)
need_commit = True
if args.autoyes or interactive.getpermission('Commit?'): if not need_commit:
photodb.commit() return 0
if not (args.autoyes or interactive.getpermission('Commit?')):
photodb.rollback()
return 1
return 0 return 0
@ -216,41 +234,46 @@ def digest_directory_argparse(args):
load_photodb() load_photodb()
need_commit = False need_commit = False
for directory in directories: with photodb.transaction:
digest = photodb.digest_directory( for directory in directories:
directory, digest = photodb.digest_directory(
exclude_directories=args.exclude_directories, directory,
exclude_filenames=args.exclude_filenames, exclude_directories=args.exclude_directories,
glob_directories=args.glob_directories, exclude_filenames=args.exclude_filenames,
glob_filenames=args.glob_filenames, glob_directories=args.glob_directories,
hash_kwargs={'bytes_per_second': args.hash_bytes_per_second}, glob_filenames=args.glob_filenames,
make_albums=args.make_albums, hash_kwargs={'bytes_per_second': args.hash_bytes_per_second},
new_photo_ratelimit=args.ratelimit, make_albums=args.make_albums,
recurse=args.recurse, new_photo_ratelimit=args.ratelimit,
yield_albums=True, recurse=args.recurse,
yield_photos=True, yield_albums=True,
) yield_photos=True,
for result in digest: )
# print(result) for result in digest:
need_commit = True # print(result)
need_commit = True
if not need_commit: if not need_commit:
return 0 return 0
if args.autoyes or interactive.getpermission('Commit?'): if not (args.autoyes or interactive.getpermission('Commit?')):
photodb.commit() photodb.rollback()
return 1
return 0 return 0
def easybake_argparse(args): def easybake_argparse(args):
load_photodb() load_photodb()
for eb_string in args.eb_strings:
notes = photodb.easybake(eb_string)
for (action, tagname) in notes:
print(action, tagname)
if args.autoyes or interactive.getpermission('Commit?'): with photodb.transaction:
photodb.commit() for eb_string in args.eb_strings:
notes = photodb.easybake(eb_string)
for (action, tagname) in notes:
print(action, tagname)
if not (args.autoyes or interactive.getpermission('Commit?')):
photodb.rollback()
return 1
return 0 return 0
@ -310,18 +333,21 @@ def generate_thumbnail_argparse(args):
photos = search_in_cwd(yield_photos=True, yield_albums=False) photos = search_in_cwd(yield_photos=True, yield_albums=False)
need_commit = False need_commit = False
try:
for photo in photos:
photo.generate_thumbnail()
need_commit = True
except KeyboardInterrupt:
pass
if not need_commit: with photodb.transaction:
return 0 try:
for photo in photos:
photo.generate_thumbnail()
need_commit = True
except KeyboardInterrupt:
pass
if args.autoyes or interactive.getpermission('Commit?'): if not need_commit:
photodb.commit() return 0
if not (args.autoyes or interactive.getpermission('Commit?')):
photodb.rollback()
return 1
return 0 return 0
@ -335,7 +361,6 @@ def init_argparse(args):
pipeable.stderr(f'PhotoDB {photodb} already exists.') pipeable.stderr(f'PhotoDB {photodb} already exists.')
return 0 return 0
photodb = etiquette.photodb.PhotoDB(create=True) photodb = etiquette.photodb.PhotoDB(create=True)
photodb.commit()
return 0 return 0
def purge_deleted_files_argparse(args): def purge_deleted_files_argparse(args):
@ -348,15 +373,17 @@ def purge_deleted_files_argparse(args):
need_commit = False need_commit = False
for deleted in photodb.purge_deleted_files(photos): with photodb.transaction:
need_commit = True for deleted in photodb.purge_deleted_files(photos):
print(deleted) need_commit = True
print(deleted)
if not need_commit: if not need_commit:
return 0 return 0
if args.autoyes or interactive.getpermission('Commit?'): if not (args.autoyes or interactive.getpermission('Commit?')):
photodb.commit() photodb.rollback()
return 1
return 0 return 0
@ -373,25 +400,24 @@ def purge_empty_albums_argparse(args):
need_commit = False need_commit = False
for deleted in photodb.purge_empty_albums(albums): with photodb.transaction:
need_commit = True for deleted in photodb.purge_empty_albums(albums):
print(deleted) need_commit = True
print(deleted)
if not need_commit: if not need_commit:
return 0 return 0
if args.autoyes or interactive.getpermission('Commit?'): if not (args.autoyes or interactive.getpermission('Commit?')):
photodb.commit() photodb.rollback()
return 1
return 0 return 0
def reload_metadata_argparse(args): def reload_metadata_argparse(args):
load_photodb() load_photodb()
if args.any_photo_args: photos = get_photos_from_args(args)
photos = get_photos_from_args(args)
else:
photos = search_in_cwd(yield_photos=True, yield_albums=False)
hash_kwargs = { hash_kwargs = {
'bytes_per_second': args.hash_bytes_per_second, 'bytes_per_second': args.hash_bytes_per_second,
@ -399,40 +425,45 @@ def reload_metadata_argparse(args):
} }
need_commit = False need_commit = False
try:
for photo in photos:
if not photo.real_path.is_file:
continue
need_reload = ( with photodb.transaction:
args.force or try:
photo.mtime != photo.real_path.stat.st_mtime or for photo in photos:
photo.bytes != photo.real_path.stat.st_size if not photo.real_path.is_file:
) continue
if not need_reload: need_reload = (
continue args.force or
photo.reload_metadata(hash_kwargs=hash_kwargs) photo.mtime != photo.real_path.stat.st_mtime or
need_commit = True photo.bytes != photo.real_path.stat.st_size
except KeyboardInterrupt: )
pass
if not need_commit: if not need_reload:
return 0 continue
photo.reload_metadata(hash_kwargs=hash_kwargs)
need_commit = True
except KeyboardInterrupt:
pass
if args.autoyes or interactive.getpermission('Commit?'): if not need_commit:
photodb.commit() return 0
if not (args.autoyes or interactive.getpermission('Commit?')):
photodb.rollback()
return 1
return 0 return 0
def relocate_argparse(args): def relocate_argparse(args):
load_photodb() load_photodb()
photo = photodb.get_photo(args.photo_id) with photodb.transaction:
photo.relocate(args.filepath) photo = photodb.get_photo(args.photo_id)
photo.relocate(args.filepath)
if args.autoyes or interactive.getpermission('Commit?'): if not (args.autoyes or interactive.getpermission('Commit?')):
photodb.commit() photodb.rollback()
return 1
return 0 return 0
@ -477,12 +508,14 @@ def set_unset_searchhidden_argparse(args, searchhidden):
else: else:
photos = search_in_cwd(yield_photos=True, yield_albums=False) photos = search_in_cwd(yield_photos=True, yield_albums=False)
for photo in photos: with photodb.transaction:
print(photo) for photo in photos:
photo.set_searchhidden(searchhidden) print(photo)
photo.set_searchhidden(searchhidden)
if args.autoyes or interactive.getpermission('Commit?'): if not (args.autoyes or interactive.getpermission('Commit?')):
photodb.commit() photodb.rollback()
return 1
return 0 return 0
@ -514,17 +547,19 @@ def tag_breplace_argparse(args):
for (tag_name, new_name, printline) in renames: for (tag_name, new_name, printline) in renames:
print(printline) print(printline)
if not interactive.getpermission('Ok?', must_pick=True): if not interactive.getpermission('Ok?', must_pick=True):
return 0 return 1
for (tag_name, new_name, printline) in renames: with photodb.transaction:
print(printline) for (tag_name, new_name, printline) in renames:
tag = photodb.get_tag(tag_name) print(printline)
tag.rename(new_name) tag = photodb.get_tag(tag_name)
if args.set_synonym: tag.rename(new_name)
tag.add_synonym(tag_name) if args.set_synonym:
tag.add_synonym(tag_name)
if args.autoyes or interactive.getpermission('Commit?'): if not (args.autoyes or interactive.getpermission('Commit?')):
photodb.commit() photodb.rollback()
return 1
return 0 return 0
@ -1577,17 +1612,17 @@ def main(argv):
## ##
def postprocessor(args): def postprocessor(args):
if hasattr(args, 'photo_search_args'): if getattr(args, 'photo_search_args', None) is not None:
args.photo_search_args = p_search.parse_args(args.photo_search_args) args.photo_search_args = p_search.parse_args(args.photo_search_args)
else: else:
args.photo_search_args = None args.photo_search_args = None
if hasattr(args, 'album_search_args'): if getattr(args, 'album_search_args', None) is not None:
args.album_search_args = p_search.parse_args(args.album_search_args) args.album_search_args = p_search.parse_args(args.album_search_args)
else: else:
args.album_search_args = None args.album_search_args = None
if hasattr(args, 'photo_id_args'): if getattr(args, 'photo_id_args', None) is not None:
args.photo_id_args = [ args.photo_id_args = [
photo_id photo_id
for arg in args.photo_id_args for arg in args.photo_id_args
@ -1596,7 +1631,7 @@ def main(argv):
else: else:
args.photo_id_args = None args.photo_id_args = None
if hasattr(args, 'album_id_args'): if getattr(args, 'album_id_args', None) is not None:
args.album_id_args = [ args.album_id_args = [
album_id album_id
for arg in args.album_id_args for arg in args.album_id_args
@ -1605,11 +1640,10 @@ def main(argv):
else: else:
args.album_id_args = None args.album_id_args = None
if not getattr(args, 'globs', None) is not None:
if not hasattr(args, 'globs'):
args.globs = None args.globs = None
if not hasattr(args, 'glob'): if not getattr(args, 'glob', None) is not None:
args.glob = None args.glob = None
args.any_photo_args = bool( args.any_photo_args = bool(
@ -1625,7 +1659,7 @@ def main(argv):
return args return args
try: try:
return betterhelp.go(parser, argv) return betterhelp.go(parser, argv, args_postprocessor=postprocessor)
except etiquette.exceptions.NoClosestPhotoDB as exc: except etiquette.exceptions.NoClosestPhotoDB as exc:
pipeable.stderr(exc.error_message) pipeable.stderr(exc.error_message)
pipeable.stderr('Try `etiquette_cli.py init` to create the database.') pipeable.stderr('Try `etiquette_cli.py init` to create the database.')

View file

@ -58,51 +58,52 @@ def get_album_zip(album_id):
@site.route('/album/<album_id>/add_child', methods=['POST']) @site.route('/album/<album_id>/add_child', methods=['POST'])
@flasktools.required_fields(['child_id'], forbid_whitespace=True) @flasktools.required_fields(['child_id'], forbid_whitespace=True)
def post_album_add_child(album_id): def post_album_add_child(album_id):
album = common.P_album(album_id, response_type='json')
child_ids = stringtools.comma_space_split(request.form['child_id']) child_ids = stringtools.comma_space_split(request.form['child_id'])
children = list(common.P_albums(child_ids, response_type='json')) with common.P.transaction:
print(children) album = common.P_album(album_id, response_type='json')
album.add_children(children, commit=True) children = list(common.P_albums(child_ids, response_type='json'))
print(children)
album.add_children(children)
response = album.jsonify() response = album.jsonify()
return flasktools.json_response(response) return flasktools.json_response(response)
@site.route('/album/<album_id>/remove_child', methods=['POST']) @site.route('/album/<album_id>/remove_child', methods=['POST'])
@flasktools.required_fields(['child_id'], forbid_whitespace=True) @flasktools.required_fields(['child_id'], forbid_whitespace=True)
def post_album_remove_child(album_id): def post_album_remove_child(album_id):
album = common.P_album(album_id, response_type='json')
child_ids = stringtools.comma_space_split(request.form['child_id']) child_ids = stringtools.comma_space_split(request.form['child_id'])
children = list(common.P_albums(child_ids, response_type='json')) with common.P.transaction:
album.remove_children(children, commit=True) album = common.P_album(album_id, response_type='json')
children = list(common.P_albums(child_ids, response_type='json'))
album.remove_children(children)
response = album.jsonify() response = album.jsonify()
return flasktools.json_response(response) return flasktools.json_response(response)
@site.route('/album/<album_id>/remove_thumbnail_photo', methods=['POST']) @site.route('/album/<album_id>/remove_thumbnail_photo', methods=['POST'])
def post_album_remove_thumbnail_photo(album_id): def post_album_remove_thumbnail_photo(album_id):
album = common.P_album(album_id, response_type='json') with common.P.transaction:
album.set_thumbnail_photo(None) album = common.P_album(album_id, response_type='json')
common.P.commit(message='album remove thumbnail photo endpoint') album.set_thumbnail_photo(None)
return flasktools.json_response(album.jsonify()) return flasktools.json_response(album.jsonify())
@site.route('/album/<album_id>/refresh_directories', methods=['POST']) @site.route('/album/<album_id>/refresh_directories', methods=['POST'])
def post_album_refresh_directories(album_id): def post_album_refresh_directories(album_id):
album = common.P_album(album_id, response_type='json') with common.P.transaction:
for directory in album.get_associated_directories(): album = common.P_album(album_id, response_type='json')
if not directory.is_dir: for directory in album.get_associated_directories():
continue if not directory.is_dir:
digest = common.P.digest_directory(directory, new_photo_ratelimit=0.1) continue
gentools.run(digest) digest = common.P.digest_directory(directory, new_photo_ratelimit=0.1)
common.P.commit(message='refresh album directories endpoint') gentools.run(digest)
return flasktools.json_response({}) return flasktools.json_response({})
@site.route('/album/<album_id>/set_thumbnail_photo', methods=['POST']) @site.route('/album/<album_id>/set_thumbnail_photo', methods=['POST'])
@flasktools.required_fields(['photo_id'], forbid_whitespace=True) @flasktools.required_fields(['photo_id'], forbid_whitespace=True)
def post_album_set_thumbnail_photo(album_id): def post_album_set_thumbnail_photo(album_id):
album = common.P_album(album_id, response_type='json') with common.P.transaction:
photo = common.P_photo(request.form['photo_id'], response_type='json') album = common.P_album(album_id, response_type='json')
album.set_thumbnail_photo(photo) photo = common.P_photo(request.form['photo_id'], response_type='json')
common.P.commit(message='album set thumbnail photo endpoint') album.set_thumbnail_photo(photo)
return flasktools.json_response(album.jsonify()) return flasktools.json_response(album.jsonify())
# Album photo operations ########################################################################### # Album photo operations ###########################################################################
@ -113,11 +114,12 @@ def post_album_add_photo(album_id):
''' '''
Add a photo or photos to this album. Add a photo or photos to this album.
''' '''
album = common.P_album(album_id, response_type='json')
photo_ids = stringtools.comma_space_split(request.form['photo_id']) photo_ids = stringtools.comma_space_split(request.form['photo_id'])
photos = list(common.P_photos(photo_ids, response_type='json')) with common.P.transaction:
album.add_photos(photos, commit=True) album = common.P_album(album_id, response_type='json')
photos = list(common.P_photos(photo_ids, response_type='json'))
album.add_photos(photos)
response = album.jsonify() response = album.jsonify()
return flasktools.json_response(response) return flasktools.json_response(response)
@ -127,11 +129,11 @@ def post_album_remove_photo(album_id):
''' '''
Remove a photo or photos from this album. Remove a photo or photos from this album.
''' '''
album = common.P_album(album_id, response_type='json')
photo_ids = stringtools.comma_space_split(request.form['photo_id']) photo_ids = stringtools.comma_space_split(request.form['photo_id'])
photos = list(common.P_photos(photo_ids, response_type='json')) with common.P.transaction:
album.remove_photos(photos, commit=True) album = common.P_album(album_id, response_type='json')
photos = list(common.P_photos(photo_ids, response_type='json'))
album.remove_photos(photos)
response = album.jsonify() response = album.jsonify()
return flasktools.json_response(response) return flasktools.json_response(response)
@ -143,17 +145,18 @@ def post_album_add_tag(album_id):
Apply a tag to every photo in the album. Apply a tag to every photo in the album.
''' '''
response = {} response = {}
album = common.P_album(album_id, response_type='json') with common.P.transaction:
album = common.P_album(album_id, response_type='json')
tag = request.form['tagname'].strip() tag = request.form['tagname'].strip()
try: try:
tag = common.P_tag(tag, response_type='json') tag = common.P_tag(tag, response_type='json')
except etiquette.exceptions.NoSuchTag as exc: except etiquette.exceptions.NoSuchTag as exc:
response = exc.jsonify() response = exc.jsonify()
return flasktools.json_response(response, status=404) return flasktools.json_response(response, status=404)
recursive = request.form.get('recursive', False) recursive = request.form.get('recursive', False)
recursive = stringtools.truthystring(recursive) recursive = stringtools.truthystring(recursive)
album.add_tag_to_all(tag, nested_children=recursive, commit=True) album.add_tag_to_all(tag, nested_children=recursive)
response['action'] = 'add_tag' response['action'] = 'add_tag'
response['tagname'] = tag.name response['tagname'] = tag.name
return flasktools.json_response(response) return flasktools.json_response(response)
@ -165,11 +168,13 @@ def post_album_edit(album_id):
''' '''
Edit the title / description. Edit the title / description.
''' '''
album = common.P_album(album_id, response_type='json')
title = request.form.get('title', None) title = request.form.get('title', None)
description = request.form.get('description', None) description = request.form.get('description', None)
album.edit(title=title, description=description, commit=True)
with common.P.transaction:
album = common.P_album(album_id, response_type='json')
album.edit(title=title, description=description)
response = album.jsonify(minimal=True) response = album.jsonify(minimal=True)
return flasktools.json_response(response) return flasktools.json_response(response)
@ -234,16 +239,17 @@ def post_albums_create():
user = session_manager.get(request).user user = session_manager.get(request).user
album = common.P.new_album(title=title, description=description, author=user) with common.P.transaction:
if parent_id is not None: album = common.P.new_album(title=title, description=description, author=user)
parent.add_child(album) if parent_id is not None:
common.P.commit('create album endpoint') parent.add_child(album)
response = album.jsonify(minimal=False) response = album.jsonify(minimal=False)
return flasktools.json_response(response) return flasktools.json_response(response)
@site.route('/album/<album_id>/delete', methods=['POST']) @site.route('/album/<album_id>/delete', methods=['POST'])
def post_album_delete(album_id): def post_album_delete(album_id):
album = common.P_album(album_id, response_type='json') with common.P.transaction:
album.delete(commit=True) album = common.P_album(album_id, response_type='json')
album.delete()
return flasktools.json_response({}) return flasktools.json_response({})

View file

@ -19,11 +19,12 @@ def get_bookmark_json(bookmark_id):
@site.route('/bookmark/<bookmark_id>/edit', methods=['POST']) @site.route('/bookmark/<bookmark_id>/edit', methods=['POST'])
def post_bookmark_edit(bookmark_id): def post_bookmark_edit(bookmark_id):
bookmark = common.P_bookmark(bookmark_id, response_type='json') with common.P.transaction:
# Emptystring is okay for titles, but not for URL. bookmark = common.P_bookmark(bookmark_id, response_type='json')
title = request.form.get('title', None) # Emptystring is okay for titles, but not for URL.
url = request.form.get('url', None) or None title = request.form.get('title', None)
bookmark.edit(title=title, url=url, commit=True) url = request.form.get('url', None) or None
bookmark.edit(title=title, url=url)
response = bookmark.jsonify() response = bookmark.jsonify()
response = flasktools.json_response(response) response = flasktools.json_response(response)
@ -49,13 +50,15 @@ def post_bookmark_create():
url = request.form['url'] url = request.form['url']
title = request.form.get('title', None) title = request.form.get('title', None)
user = session_manager.get(request).user user = session_manager.get(request).user
bookmark = common.P.new_bookmark(url=url, title=title, author=user, commit=True) with common.P.transaction:
bookmark = common.P.new_bookmark(url=url, title=title, author=user)
response = bookmark.jsonify() response = bookmark.jsonify()
response = flasktools.json_response(response) response = flasktools.json_response(response)
return response return response
@site.route('/bookmark/<bookmark_id>/delete', methods=['POST']) @site.route('/bookmark/<bookmark_id>/delete', methods=['POST'])
def post_bookmark_delete(bookmark_id): def post_bookmark_delete(bookmark_id):
bookmark = common.P_bookmark(bookmark_id, response_type='json') with common.P.transaction:
bookmark.delete(commit=True) bookmark = common.P_bookmark(bookmark_id, response_type='json')
bookmark.delete()
return flasktools.json_response({}) return flasktools.json_response({})

View file

@ -75,11 +75,11 @@ def get_thumbnail(photo_id):
@site.route('/photo/<photo_id>/delete', methods=['POST']) @site.route('/photo/<photo_id>/delete', methods=['POST'])
def post_photo_delete(photo_id): def post_photo_delete(photo_id):
print(photo_id)
photo = common.P_photo(photo_id, response_type='json')
delete_file = request.form.get('delete_file', False) delete_file = request.form.get('delete_file', False)
delete_file = stringtools.truthystring(delete_file) delete_file = stringtools.truthystring(delete_file)
photo.delete(delete_file=delete_file, commit=True) with common.P.transaction:
photo = common.P_photo(photo_id, response_type='json')
photo.delete(delete_file=delete_file)
return flasktools.json_response({}) return flasktools.json_response({})
# Photo tag operations ############################################################################# # Photo tag operations #############################################################################
@ -91,12 +91,12 @@ def post_photo_add_remove_tag_core(photo_ids, tagname, add_or_remove):
photos = list(common.P_photos(photo_ids, response_type='json')) photos = list(common.P_photos(photo_ids, response_type='json'))
tag = common.P_tag(tagname, response_type='json') tag = common.P_tag(tagname, response_type='json')
for photo in photos: with common.P.transaction:
if add_or_remove == 'add': for photo in photos:
photo.add_tag(tag) if add_or_remove == 'add':
elif add_or_remove == 'remove': photo.add_tag(tag)
photo.remove_tag(tag) elif add_or_remove == 'remove':
common.P.commit('photo add remove tag core') photo.remove_tag(tag)
response = {'action': add_or_remove, 'tagname': tag.name} response = {'action': add_or_remove, 'tagname': tag.name}
return flasktools.json_response(response) return flasktools.json_response(response)
@ -120,10 +120,10 @@ def post_photo_copy_tags(photo_id):
''' '''
Copy the tags from another photo. Copy the tags from another photo.
''' '''
photo = common.P_photo(photo_id, response_type='json') with common.P.transaction:
other = common.P_photo(request.form['other_photo'], response_type='json') photo = common.P_photo(photo_id, response_type='json')
photo.copy_tags(other) other = common.P_photo(request.form['other_photo'], response_type='json')
common.P.commit('photo copy tags') photo.copy_tags(other)
return flasktools.json_response([tag.jsonify(minimal=True) for tag in photo.get_tags()]) return flasktools.json_response([tag.jsonify(minimal=True) for tag in photo.get_tags()])
@site.route('/photo/<photo_id>/remove_tag', methods=['POST']) @site.route('/photo/<photo_id>/remove_tag', methods=['POST'])
@ -164,10 +164,10 @@ def post_batch_photos_remove_tag():
@site.route('/photo/<photo_id>/generate_thumbnail', methods=['POST']) @site.route('/photo/<photo_id>/generate_thumbnail', methods=['POST'])
def post_photo_generate_thumbnail(photo_id): def post_photo_generate_thumbnail(photo_id):
special = request.form.to_dict() special = request.form.to_dict()
special.pop('commit', None)
photo = common.P_photo(photo_id, response_type='json') with common.P.transaction:
photo.generate_thumbnail(commit=True, **special) photo = common.P_photo(photo_id, response_type='json')
photo.generate_thumbnail(**special)
response = flasktools.json_response({}) response = flasktools.json_response({})
return response return response
@ -176,22 +176,21 @@ def post_photo_refresh_metadata_core(photo_ids):
if isinstance(photo_ids, str): if isinstance(photo_ids, str):
photo_ids = stringtools.comma_space_split(photo_ids) photo_ids = stringtools.comma_space_split(photo_ids)
photos = list(common.P_photos(photo_ids, response_type='json')) with common.P.transaction:
photos = list(common.P_photos(photo_ids, response_type='json'))
for photo in photos: for photo in photos:
photo._uncache() photo._uncache()
photo = common.P_photo(photo.id, response_type='json') photo = common.P_photo(photo.id, response_type='json')
try:
photo.reload_metadata()
except pathclass.NotFile:
flask.abort(404)
if photo.thumbnail is None:
try: try:
photo.generate_thumbnail() photo.reload_metadata()
except Exception: except pathclass.NotFile:
log.warning(traceback.format_exc()) flask.abort(404)
if photo.thumbnail is None:
common.P.commit('photo refresh metadata core') try:
photo.generate_thumbnail()
except Exception:
log.warning(traceback.format_exc())
return flasktools.json_response({}) return flasktools.json_response({})
@ -208,26 +207,27 @@ def post_batch_photos_refresh_metadata():
@site.route('/photo/<photo_id>/set_searchhidden', methods=['POST']) @site.route('/photo/<photo_id>/set_searchhidden', methods=['POST'])
def post_photo_set_searchhidden(photo_id): def post_photo_set_searchhidden(photo_id):
photo = common.P_photo(photo_id, response_type='json') with common.P.transaction:
photo.set_searchhidden(True) photo = common.P_photo(photo_id, response_type='json')
photo.set_searchhidden(True)
return flasktools.json_response({}) return flasktools.json_response({})
@site.route('/photo/<photo_id>/unset_searchhidden', methods=['POST']) @site.route('/photo/<photo_id>/unset_searchhidden', methods=['POST'])
def post_photo_unset_searchhidden(photo_id): def post_photo_unset_searchhidden(photo_id):
photo = common.P_photo(photo_id, response_type='json') with common.P.transaction:
photo.set_searchhidden(False) photo = common.P_photo(photo_id, response_type='json')
photo.set_searchhidden(False)
return flasktools.json_response({}) return flasktools.json_response({})
def post_batch_photos_searchhidden_core(photo_ids, searchhidden): def post_batch_photos_searchhidden_core(photo_ids, searchhidden):
if isinstance(photo_ids, str): if isinstance(photo_ids, str):
photo_ids = stringtools.comma_space_split(photo_ids) photo_ids = stringtools.comma_space_split(photo_ids)
photos = list(common.P_photos(photo_ids, response_type='json')) with common.P.transaction:
photos = list(common.P_photos(photo_ids, response_type='json'))
for photo in photos: for photo in photos:
photo.set_searchhidden(searchhidden) photo.set_searchhidden(searchhidden)
common.P.commit('photo set searchhidden core')
return flasktools.json_response({}) return flasktools.json_response({})

View file

@ -44,24 +44,25 @@ def get_tag_json(specific_tag_name):
@site.route('/tag/<tagname>/edit', methods=['POST']) @site.route('/tag/<tagname>/edit', methods=['POST'])
def post_tag_edit(tagname): def post_tag_edit(tagname):
tag = common.P_tag(tagname, response_type='json') with common.P.transaction:
name = request.form.get('name', '').strip() tag = common.P_tag(tagname, response_type='json')
if name: name = request.form.get('name', '').strip()
tag.rename(name) if name:
tag.rename(name)
description = request.form.get('description', None) description = request.form.get('description', None)
tag.edit(description=description, commit=True) tag.edit(description=description)
response = tag.jsonify() response = flasktools.json_response(tag.jsonify())
response = flasktools.json_response(response)
return response return response
@site.route('/tag/<tagname>/add_child', methods=['POST']) @site.route('/tag/<tagname>/add_child', methods=['POST'])
@flasktools.required_fields(['child_name'], forbid_whitespace=True) @flasktools.required_fields(['child_name'], forbid_whitespace=True)
def post_tag_add_child(tagname): def post_tag_add_child(tagname):
parent = common.P_tag(tagname, response_type='json') with common.P.transaction:
child = common.P_tag(request.form['child_name'], response_type='json') parent = common.P_tag(tagname, response_type='json')
parent.add_child(child, commit=True) child = common.P_tag(request.form['child_name'], response_type='json')
parent.add_child(child)
response = {'action': 'add_child', 'tagname': f'{parent.name}.{child.name}'} response = {'action': 'add_child', 'tagname': f'{parent.name}.{child.name}'}
return flasktools.json_response(response) return flasktools.json_response(response)
@ -70,8 +71,9 @@ def post_tag_add_child(tagname):
def post_tag_add_synonym(tagname): def post_tag_add_synonym(tagname):
syn_name = request.form['syn_name'] syn_name = request.form['syn_name']
master_tag = common.P_tag(tagname, response_type='json') with common.P.transaction:
syn_name = master_tag.add_synonym(syn_name, commit=True) master_tag = common.P_tag(tagname, response_type='json')
syn_name = master_tag.add_synonym(syn_name)
response = {'action': 'add_synonym', 'synonym': syn_name} response = {'action': 'add_synonym', 'synonym': syn_name}
return flasktools.json_response(response) return flasktools.json_response(response)
@ -79,9 +81,10 @@ def post_tag_add_synonym(tagname):
@site.route('/tag/<tagname>/remove_child', methods=['POST']) @site.route('/tag/<tagname>/remove_child', methods=['POST'])
@flasktools.required_fields(['child_name'], forbid_whitespace=True) @flasktools.required_fields(['child_name'], forbid_whitespace=True)
def post_tag_remove_child(tagname): def post_tag_remove_child(tagname):
parent = common.P_tag(tagname, response_type='json') with common.P.transaction:
child = common.P_tag(request.form['child_name'], response_type='json') parent = common.P_tag(tagname, response_type='json')
parent.remove_child(child, commit=True) child = common.P_tag(request.form['child_name'], response_type='json')
parent.remove_child(child)
response = {'action': 'remove_child', 'tagname': f'{parent.name}.{child.name}'} response = {'action': 'remove_child', 'tagname': f'{parent.name}.{child.name}'}
return flasktools.json_response(response) return flasktools.json_response(response)
@ -90,8 +93,9 @@ def post_tag_remove_child(tagname):
def post_tag_remove_synonym(tagname): def post_tag_remove_synonym(tagname):
syn_name = request.form['syn_name'] syn_name = request.form['syn_name']
master_tag = common.P_tag(tagname, response_type='json') with common.P.transaction:
syn_name = master_tag.remove_synonym(syn_name, commit=True) master_tag = common.P_tag(tagname, response_type='json')
syn_name = master_tag.remove_synonym(syn_name)
response = {'action': 'delete_synonym', 'synonym': syn_name} response = {'action': 'delete_synonym', 'synonym': syn_name}
return flasktools.json_response(response) return flasktools.json_response(response)
@ -163,7 +167,8 @@ def post_tag_create():
name = request.form['name'] name = request.form['name']
description = request.form.get('description', None) description = request.form.get('description', None)
tag = common.P.new_tag(name, description, author=session_manager.get(request).user, commit=True) with common.P.transaction:
tag = common.P.new_tag(name, description, author=session_manager.get(request).user)
response = tag.jsonify() response = tag.jsonify()
return flasktools.json_response(response) return flasktools.json_response(response)
@ -172,13 +177,15 @@ def post_tag_create():
def post_tag_easybake(): def post_tag_easybake():
easybake_string = request.form['easybake_string'] easybake_string = request.form['easybake_string']
notes = common.P.easybake(easybake_string, author=session_manager.get(request).user, commit=True) with common.P.transaction:
notes = common.P.easybake(easybake_string, author=session_manager.get(request).user)
notes = [{'action': action, 'tagname': tagname} for (action, tagname) in notes] notes = [{'action': action, 'tagname': tagname} for (action, tagname) in notes]
return flasktools.json_response(notes) return flasktools.json_response(notes)
@site.route('/tag/<tagname>/delete', methods=['POST']) @site.route('/tag/<tagname>/delete', methods=['POST'])
def post_tag_delete(tagname): def post_tag_delete(tagname):
tag = common.P_tag(tagname, response_type='json') with common.P.transaction:
tag.delete(commit=True) tag = common.P_tag(tagname, response_type='json')
tag.delete()
response = {'action': 'delete_tag', 'tagname': tag.name} response = {'action': 'delete_tag', 'tagname': tag.name}
return flasktools.json_response(response) return flasktools.json_response(response)

View file

@ -47,9 +47,8 @@ def post_user_edit(username):
display_name = request.form.get('display_name') display_name = request.form.get('display_name')
if display_name is not None: if display_name is not None:
user.set_display_name(display_name) with common.P.transaction:
user.set_display_name(display_name)
common.P.commit()
return flasktools.json_response(user.jsonify()) return flasktools.json_response(user.jsonify())
@ -127,7 +126,8 @@ def post_register():
} }
return flasktools.json_response(response, status=422) return flasktools.json_response(response, status=422)
user = common.P.new_user(username, password_1, display_name=display_name, commit=True) with common.P.transaction:
user = common.P.new_user(username, password_1, display_name=display_name)
session = sessions.Session(request, user) session = sessions.Session(request, user)
session_manager.add(session) session_manager.add(session)

View file

@ -2,8 +2,12 @@ import argparse
import os import os
import sys import sys
from voussoirkit import vlogging
import etiquette import etiquette
log = vlogging.get_logger(__name__, 'database_upgrader')
class Migrator: class Migrator:
''' '''
Many of the upgraders involve adding columns. ALTER TABLE ADD COLUMN only Many of the upgraders involve adding columns. ALTER TABLE ADD COLUMN only
@ -45,8 +49,7 @@ class Migrator:
# be pointing to the version of B which has not been reconstructed yet, # be pointing to the version of B which has not been reconstructed yet,
# which is about to get renamed to B_old and then A's reference will be # which is about to get renamed to B_old and then A's reference will be
# broken. # broken.
self.photodb.execute('PRAGMA foreign_keys = OFF') self.photodb.pragma_write('foreign_keys', 'OFF')
self.photodb.execute('BEGIN')
for (name, table) in self.tables.items(): for (name, table) in self.tables.items():
if name not in self.existing_tables: if name not in self.existing_tables:
continue continue
@ -65,16 +68,14 @@ class Migrator:
for (name, query) in self.indices: for (name, query) in self.indices:
self.photodb.execute(query) self.photodb.execute(query)
self.photodb.pragma_write('foreign_keys', 'ON')
def upgrade_1_to_2(photodb): def upgrade_1_to_2(photodb):
''' '''
In this version, a column `tagged_at` was added to the Photos table, to keep In this version, a column `tagged_at` was added to the Photos table, to keep
track of the last time the photo's tags were edited (added or removed). track of the last time the photo's tags were edited (added or removed).
''' '''
photodb.executescript(''' photodb.execute('ALTER TABLE photos ADD COLUMN tagged_at INT')
BEGIN;
ALTER TABLE photos ADD COLUMN tagged_at INT;
''')
def upgrade_2_to_3(photodb): def upgrade_2_to_3(photodb):
''' '''
@ -83,8 +84,6 @@ def upgrade_2_to_3(photodb):
Plus some indices. Plus some indices.
''' '''
photodb.executescript(''' photodb.executescript('''
BEGIN;
CREATE TABLE users( CREATE TABLE users(
id TEXT, id TEXT,
username TEXT COLLATE NOCASE, username TEXT COLLATE NOCASE,
@ -102,8 +101,6 @@ def upgrade_3_to_4(photodb):
Add an `author_id` column to Photos. Add an `author_id` column to Photos.
''' '''
photodb.executescript(''' photodb.executescript('''
BEGIN;
ALTER TABLE photos ADD COLUMN author_id TEXT; ALTER TABLE photos ADD COLUMN author_id TEXT;
CREATE INDEX IF NOT EXISTS index_photo_author ON photos(author_id); CREATE INDEX IF NOT EXISTS index_photo_author ON photos(author_id);
@ -114,8 +111,6 @@ def upgrade_4_to_5(photodb):
Add table `bookmarks` and its indices. Add table `bookmarks` and its indices.
''' '''
photodb.executescript(''' photodb.executescript('''
BEGIN;
CREATE TABLE bookmarks( CREATE TABLE bookmarks(
id TEXT, id TEXT,
title TEXT, title TEXT,
@ -259,18 +254,13 @@ def upgrade_7_to_8(photodb):
''' '''
Give the Tags table a description field. Give the Tags table a description field.
''' '''
photodb.executescript(''' photodb.executescript('ALTER TABLE tags ADD COLUMN description TEXT')
BEGIN;
ALTER TABLE tags ADD COLUMN description TEXT;
''')
def upgrade_8_to_9(photodb): def upgrade_8_to_9(photodb):
''' '''
Give the Photos table a searchhidden field. Give the Photos table a searchhidden field.
''' '''
photodb.executescript(''' photodb.executescript('''
BEGIN;
ALTER TABLE photos ADD COLUMN searchhidden INT; ALTER TABLE photos ADD COLUMN searchhidden INT;
UPDATE photos SET searchhidden = 0; UPDATE photos SET searchhidden = 0;
@ -351,9 +341,7 @@ def upgrade_11_to_12(photodb):
improve the speed of individual relation searching, important for the improve the speed of individual relation searching, important for the
new intersection-based search. new intersection-based search.
''' '''
photodb.executescript(''' photodb.execute('''
BEGIN;
CREATE INDEX IF NOT EXISTS index_photo_tag_rel_photoid_tagid on photo_tag_rel(photoid, tagid); CREATE INDEX IF NOT EXISTS index_photo_tag_rel_photoid_tagid on photo_tag_rel(photoid, tagid);
''') ''')
@ -679,11 +667,12 @@ def upgrade_all(data_directory):
''' '''
photodb = etiquette.photodb.PhotoDB(data_directory, create=False, skip_version_check=True) photodb = etiquette.photodb.PhotoDB(data_directory, create=False, skip_version_check=True)
current_version = photodb.execute('PRAGMA user_version').fetchone()[0] current_version = photodb.pragma_read('user_version')
needed_version = etiquette.constants.DATABASE_VERSION needed_version = etiquette.constants.DATABASE_VERSION
if current_version == needed_version: if current_version == needed_version:
print('Already up to date with version %d.' % needed_version) print('Already up to date with version %d.' % needed_version)
photodb.close()
return return
for version_number in range(current_version + 1, needed_version + 1): for version_number in range(current_version + 1, needed_version + 1):
@ -691,22 +680,20 @@ def upgrade_all(data_directory):
upgrade_function = 'upgrade_%d_to_%d' % (current_version, version_number) upgrade_function = 'upgrade_%d_to_%d' % (current_version, version_number)
upgrade_function = eval(upgrade_function) upgrade_function = eval(upgrade_function)
try: photodb.pragma_write('journal_mode', 'wal')
photodb.execute('PRAGMA foreign_keys = ON') with photodb.transaction:
photodb.pragma_write('foreign_keys', 'ON')
upgrade_function(photodb) upgrade_function(photodb)
except Exception as exc: photodb.pragma_write('user_version', version_number)
photodb.rollback()
raise
else:
photodb.sql.cursor().execute('PRAGMA user_version = %d' % version_number)
photodb.commit()
current_version = version_number current_version = version_number
photodb.close()
print('Upgrades finished.') print('Upgrades finished.')
def upgrade_all_argparse(args): def upgrade_all_argparse(args):
return upgrade_all(data_directory=args.data_directory) return upgrade_all(data_directory=args.data_directory)
@vlogging.main_decorator
def main(argv): def main(argv):
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()