diff --git a/etiquette/constants.py b/etiquette/constants.py index 08bd6a1..cbf29b0 100644 --- a/etiquette/constants.py +++ b/etiquette/constants.py @@ -42,19 +42,8 @@ ffmpeg = _load_ffmpeg() # Database ######################################################################################### 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''' -BEGIN; -{DB_PRAGMAS} -{DB_VERSION_PRAGMA} ---------------------------------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS albums( id TEXT PRIMARY KEY NOT NULL, @@ -191,8 +180,6 @@ CREATE TABLE IF NOT EXISTS tag_synonyms( mastername TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS index_tag_synonyms_name on tag_synonyms(name); ----------------------------------------------------------------------------------------------------- -COMMIT; ''' SQL_COLUMNS = sqlhelpers.extract_table_column_map(DB_INIT) diff --git a/etiquette/objects.py b/etiquette/objects.py index e17b583..5136377 100644 --- a/etiquette/objects.py +++ b/etiquette/objects.py @@ -329,7 +329,7 @@ class Album(ObjectBase, GroupableMixin): self.photodb.insert(table='album_associated_directories', data=data) @decorators.required_feature('album.edit') - @worms.transaction + @worms.atomic def add_associated_directory(self, path) -> None: ''' 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) @decorators.required_feature('album.edit') - @worms.transaction + @worms.atomic def add_associated_directories(self, paths) -> None: ''' Add multiple associated directories. @@ -352,7 +352,7 @@ class Album(ObjectBase, GroupableMixin): self._add_associated_directory(path) @decorators.required_feature('album.edit') - @worms.transaction + @worms.atomic def add_child(self, member): ''' Raises exceptions.CantGroupSelf if member is self. @@ -362,7 +362,7 @@ class Album(ObjectBase, GroupableMixin): return super().add_child(member) @decorators.required_feature('album.edit') - @worms.transaction + @worms.atomic def add_children(self, *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) @decorators.required_feature('album.edit') - @worms.transaction + @worms.atomic def add_photo(self, photo) -> None: if self.has_photo(photo): return @@ -380,7 +380,7 @@ class Album(ObjectBase, GroupableMixin): self._add_photo(photo) @decorators.required_feature('album.edit') - @worms.transaction + @worms.atomic def add_photos(self, photos) -> None: existing_photos = set(self.get_photos()) photos = set(photos) @@ -393,7 +393,7 @@ class Album(ObjectBase, GroupableMixin): self._add_photo(photo) # Photo.add_tag already has @required_feature - @worms.transaction + @worms.atomic 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 @@ -413,7 +413,7 @@ class Album(ObjectBase, GroupableMixin): photo.add_tag(tag) @decorators.required_feature('album.edit') - @worms.transaction + @worms.atomic def delete(self, *, delete_children=False) -> None: log.info('Deleting %s.', self) GroupableMixin.delete(self, delete_children=delete_children) @@ -431,7 +431,7 @@ class Album(ObjectBase, GroupableMixin): return self.id @decorators.required_feature('album.edit') - @worms.transaction + @worms.atomic def edit(self, title=None, description=None) -> None: ''' Change the title or description. Leave None to keep current value. @@ -546,12 +546,12 @@ class Album(ObjectBase, GroupableMixin): return j @decorators.required_feature('album.edit') - @worms.transaction + @worms.atomic def remove_child(self, *args, **kwargs): return super().remove_child(*args, **kwargs) @decorators.required_feature('album.edit') - @worms.transaction + @worms.atomic def remove_children(self, *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) @decorators.required_feature('album.edit') - @worms.transaction + @worms.atomic def remove_photo(self, photo) -> None: self._remove_photo(photo) @decorators.required_feature('album.edit') - @worms.transaction + @worms.atomic def remove_photos(self, photos) -> None: existing_photos = set(self.get_photos()) photos = set(photos) @@ -579,7 +579,7 @@ class Album(ObjectBase, GroupableMixin): self._remove_photo(photo) @decorators.required_feature('album.edit') - @worms.transaction + @worms.atomic def set_thumbnail_photo(self, photo) -> None: ''' Raises TypeError if photo is not a Photo. @@ -747,7 +747,7 @@ class Bookmark(ObjectBase): self.photodb.caches[Bookmark].remove(self.id) @decorators.required_feature('bookmark.edit') - @worms.transaction + @worms.atomic def delete(self) -> None: self.photodb.delete(table=Bookmark, pairs={'id': self.id}) self._uncache() @@ -761,7 +761,7 @@ class Bookmark(ObjectBase): return self.id @decorators.required_feature('bookmark.edit') - @worms.transaction + @worms.atomic def edit(self, title=None, url=None) -> None: ''' 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. @decorators.required_feature('photo.add_remove_tag') - @worms.transaction + @worms.atomic def add_tag(self, tag): tag = self.photodb.get_tag(name=tag) @@ -948,7 +948,7 @@ class Photo(ObjectBase): return '??? b' # Photo.add_tag already has @required_feature add_remove_tag - @worms.transaction + @worms.atomic def copy_tags(self, other_photo) -> None: ''' 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) @decorators.required_feature('photo.edit') - @worms.transaction + @worms.atomic def delete(self, *, delete_file=False) -> None: ''' 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) @decorators.required_feature('photo.generate_thumbnail') - @worms.transaction + @worms.atomic def generate_thumbnail(self, **special) -> pathclass.Path: ''' special: @@ -1144,7 +1144,7 @@ class Photo(ObjectBase): return hopeful_filepath # Photo.rename_file already has @required_feature - @worms.transaction + @worms.atomic def move_file(self, directory) -> None: directory = pathclass.Path(directory) directory.assert_is_directory() @@ -1195,7 +1195,7 @@ class Photo(ObjectBase): self.duration = probe.audio.duration @decorators.required_feature('photo.reload_metadata') - @worms.transaction + @worms.atomic def reload_metadata(self, hash_kwargs=None) -> None: ''' Load the file's height, width, etc as appropriate for this type of file. @@ -1252,7 +1252,7 @@ class Photo(ObjectBase): self._uncache() @decorators.required_feature('photo.edit') - @worms.transaction + @worms.atomic def relocate(self, new_filepath) -> None: ''' Point the Photo object to a different filepath. @@ -1287,7 +1287,7 @@ class Photo(ObjectBase): self._uncache() @decorators.required_feature('photo.add_remove_tag') - @worms.transaction + @worms.atomic def remove_tag(self, tag) -> None: tag = self.photodb.get_tag(name=tag) @@ -1305,7 +1305,7 @@ class Photo(ObjectBase): self.photodb.update(table=Photo, pairs=data, where_key='id') @decorators.required_feature('photo.add_remove_tag') - @worms.transaction + @worms.atomic def remove_tags(self, tags) -> None: 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') @decorators.required_feature('photo.edit') - @worms.transaction + @worms.atomic def rename_file(self, new_filename, *, move=False) -> None: ''' Rename the file on the disk as well as in the database. @@ -1411,7 +1411,7 @@ class Photo(ObjectBase): self.__reinit__() @decorators.required_feature('photo.edit') - @worms.transaction + @worms.atomic def set_override_filename(self, new_filename) -> None: new_filename = self.normalize_override_filename(new_filename) @@ -1425,7 +1425,7 @@ class Photo(ObjectBase): self.__reinit__() @decorators.required_feature('photo.edit') - @worms.transaction + @worms.atomic def set_searchhidden(self, searchhidden) -> None: data = { 'id': self.id, @@ -1537,7 +1537,7 @@ class Tag(ObjectBase, GroupableMixin): photo.remove_tags(ancestors) @decorators.required_feature('tag.edit') - @worms.transaction + @worms.atomic def add_child(self, member): ''' Raises exceptions.CantGroupSelf if member is self. @@ -1552,7 +1552,7 @@ class Tag(ObjectBase, GroupableMixin): return ret @decorators.required_feature('tag.edit') - @worms.transaction + @worms.atomic def add_children(self, members): ret = super().add_children(members) if ret is BAIL: @@ -1562,7 +1562,7 @@ class Tag(ObjectBase, GroupableMixin): return ret @decorators.required_feature('tag.edit') - @worms.transaction + @worms.atomic def add_synonym(self, synname) -> str: ''' Raises any exceptions from photodb.normalize_tagname. @@ -1594,7 +1594,7 @@ class Tag(ObjectBase, GroupableMixin): return synname @decorators.required_feature('tag.edit') - @worms.transaction + @worms.atomic def convert_to_synonym(self, mastertag) -> None: ''' Convert this tag into a synonym for a different tag. @@ -1656,7 +1656,7 @@ class Tag(ObjectBase, GroupableMixin): mastertag.add_synonym(self.name) @decorators.required_feature('tag.edit') - @worms.transaction + @worms.atomic def delete(self, *, delete_children=False) -> None: log.info('Deleting %s.', self) super().delete(delete_children=delete_children) @@ -1668,7 +1668,7 @@ class Tag(ObjectBase, GroupableMixin): self.deleted = True @decorators.required_feature('tag.edit') - @worms.transaction + @worms.atomic def edit(self, description=None) -> None: ''' Change the description. Leave None to keep current value. @@ -1718,7 +1718,7 @@ class Tag(ObjectBase, GroupableMixin): return j @decorators.required_feature('tag.edit') - @worms.transaction + @worms.atomic def remove_child(self, *args, **kwargs): ret = super().remove_child(*args, **kwargs) if ret is BAIL: @@ -1728,7 +1728,7 @@ class Tag(ObjectBase, GroupableMixin): return ret @decorators.required_feature('tag.edit') - @worms.transaction + @worms.atomic def remove_children(self, *args, **kwargs): ret = super().remove_children(*args, **kwargs) if ret is BAIL: @@ -1738,7 +1738,7 @@ class Tag(ObjectBase, GroupableMixin): return ret @decorators.required_feature('tag.edit') - @worms.transaction + @worms.atomic def remove_synonym(self, synname) -> str: ''' Delete a synonym. @@ -1770,7 +1770,7 @@ class Tag(ObjectBase, GroupableMixin): return synname @decorators.required_feature('tag.edit') - @worms.transaction + @worms.atomic def rename(self, new_name, *, apply_to_synonyms=True) -> None: ''' 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) @decorators.required_feature('user.edit') - @worms.transaction + @worms.atomic def delete(self, *, disown_authored_things) -> None: ''' If disown_authored_things is True then all of this user's albums, @@ -1978,7 +1978,7 @@ class User(ObjectBase): return j @decorators.required_feature('user.edit') - @worms.transaction + @worms.atomic def set_display_name(self, display_name) -> None: display_name = self.normalize_display_name( display_name, @@ -1993,7 +1993,7 @@ class User(ObjectBase): self._display_name = display_name @decorators.required_feature('user.edit') - @worms.transaction + @worms.atomic def set_password(self, password) -> None: if not isinstance(password, bytes): password = password.encode('utf-8') diff --git a/etiquette/photodb.py b/etiquette/photodb.py index 9426ba4..11685ce 100644 --- a/etiquette/photodb.py +++ b/etiquette/photodb.py @@ -9,6 +9,14 @@ import time import types 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 configlayers from voussoirkit import expressionmatch @@ -22,13 +30,6 @@ from voussoirkit import worms 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) @decorators.required_feature('album.new') - @worms.transaction + @worms.atomic def new_album( self, title=None, @@ -130,7 +131,7 @@ class PDBAlbumMixin: return album - @worms.transaction + @worms.atomic def purge_deleted_associated_directories(self, albums=None) -> typing.Iterable[pathclass.Path]: query = 'SELECT DISTINCT directory FROM album_associated_directories' directories = self.select_column(query) @@ -148,7 +149,7 @@ class PDBAlbumMixin: self.execute(query) yield from directories - @worms.transaction + @worms.atomic def purge_empty_albums(self, albums=None) -> typing.Iterable[objects.Album]: if albums is None: to_check = set(self.get_albums()) @@ -188,7 +189,7 @@ class PDBBookmarkMixin: return self.get_objects_by_sql(objects.Bookmark, query, bindings) @decorators.required_feature('bookmark.new') - @worms.transaction + @worms.atomic def new_bookmark(self, url, title=None, *, author=None) -> objects.Bookmark: # These might raise exceptions. title = objects.Bookmark.normalize_title(title) @@ -305,7 +306,7 @@ class PDBPhotoMixin: return self.get_objects_by_sql(objects.Photo, query, bindings) @decorators.required_feature('photo.new') - @worms.transaction + @worms.atomic def new_photo( self, filepath, @@ -390,7 +391,7 @@ class PDBPhotoMixin: return photo - @worms.transaction + @worms.atomic def purge_deleted_files(self, photos=None) -> typing.Iterable[objects.Photo]: ''' 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) @decorators.required_feature('tag.new') - @worms.transaction + @worms.atomic def new_tag(self, tagname, description=None, *, author=None) -> objects.Tag: ''' Register a new tag and return the Tag object. @@ -1138,7 +1139,7 @@ class PDBUserMixin: return user @decorators.required_feature('user.new') - @worms.transaction + @worms.atomic def new_user(self, username, password, *, display_name=None) -> objects.User: # These might raise exceptions. self.assert_valid_username(username) @@ -1177,7 +1178,7 @@ class PDBUtilMixin: def __init__(self): super().__init__() - @worms.transaction + @worms.atomic def digest_directory( self, directory, @@ -1436,7 +1437,7 @@ class PDBUtilMixin: if yield_albums: yield from current_albums - @worms.transaction + @worms.atomic def easybake(self, ebstring, author=None): ''' 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, 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: raise exceptions.DatabaseOutOfDate( existing=existing, @@ -1580,8 +1581,10 @@ class PhotoDB( def _first_time_setup(self): log.info('Running first-time database setup.') - self.executescript(constants.DB_INIT) - self.commit() + with self.transaction: + self._load_pragmas() + self.pragma_write('user_version', constants.DATABASE_VERSION) + self.executescript(constants.DB_INIT) def _init_caches(self): self.caches = { @@ -1600,8 +1603,8 @@ class PhotoDB( def _init_sql(self, create, skip_version_check): if self.ephemeral: existing_database = False - self.sql = sqlite3.connect(':memory:') - self.sql.row_factory = sqlite3.Row + self.sql_read = self._make_sqlite_read_connection(':memory:') + self.sql_write = self._make_sqlite_write_connection(':memory:') self._first_time_setup() return @@ -1613,21 +1616,21 @@ class PhotoDB( raise FileNotFoundError(msg) self.data_directory.makedirs(exist_ok=True) - log.debug('Connecting to sqlite file "%s".', self.database_filepath.absolute_path) - self.sql = sqlite3.connect(self.database_filepath.absolute_path) - self.sql.row_factory = sqlite3.Row + self.sql_read = self._make_sqlite_read_connection(self.database_filepath) + self.sql_write = self._make_sqlite_write_connection(self.database_filepath) if existing_database: if not skip_version_check: self._check_version() - self._load_pragmas() + with self.transaction: + self._load_pragmas() else: self._first_time_setup() def _load_pragmas(self): log.debug('Reloading pragmas.') - self.executescript(constants.DB_PRAGMAS) - self.commit() + self.pragma_write('cache_size', 10000) + self.pragma_write('foreign_keys', 'on') # Will add -> PhotoDB when forward references are supported @classmethod diff --git a/frontends/etiquette_cli.py b/frontends/etiquette_cli.py index 7bc8c62..61bd1ba 100644 --- a/frontends/etiquette_cli.py +++ b/frontends/etiquette_cli.py @@ -65,7 +65,14 @@ def get_photos_by_glob(pattern): if pattern == '**': 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: photo = photodb.get_photo_by_path(file) yield photo @@ -76,7 +83,7 @@ def get_photos_by_globs(patterns): for pattern in patterns: 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() photos = [] @@ -92,6 +99,9 @@ def get_photos_from_args(args): if args.photo_search_args: 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 def get_albums_from_args(args): @@ -158,18 +168,20 @@ def add_remove_tag_argparse(args, action): need_commit = False - for photo in photos: - if action == 'add': - photo.add_tag(tag) - elif action == 'remove': - photo.remove_tag(tag) - need_commit = True + with photodb.transaction: + for photo in photos: + if action == 'add': + photo.add_tag(tag) + elif action == 'remove': + photo.remove_tag(tag) + need_commit = True - if not need_commit: - return 0 + if not need_commit: + return 0 - if args.autoyes or interactive.getpermission('Commit?'): - photodb.commit() + if not (args.autoyes or interactive.getpermission('Commit?')): + photodb.rollback() + return 1 return 0 @@ -178,15 +190,18 @@ def delete_albums_argparse(args): need_commit = False albums = get_albums_from_args(args) - for album in albums: - album.delete() - need_commit = True - if not need_commit: - return 0 + with photodb.transaction: + for album in albums: + album.delete() + need_commit = True - if args.autoyes or interactive.getpermission('Commit?'): - photodb.commit() + if not need_commit: + return 0 + + if not (args.autoyes or interactive.getpermission('Commit?')): + photodb.rollback() + return 1 return 0 @@ -195,15 +210,18 @@ def delete_photos_argparse(args): need_commit = False photos = get_photos_from_args(args) - for photo in photos: - photo.delete(delete_file=args.delete_file) - need_commit = True - if not need_commit: - return 0 + with photodb.transaction: + for photo in photos: + photo.delete(delete_file=args.delete_file) + need_commit = True - if args.autoyes or interactive.getpermission('Commit?'): - photodb.commit() + if not need_commit: + return 0 + + if not (args.autoyes or interactive.getpermission('Commit?')): + photodb.rollback() + return 1 return 0 @@ -216,41 +234,46 @@ def digest_directory_argparse(args): load_photodb() need_commit = False - for directory in directories: - digest = photodb.digest_directory( - directory, - exclude_directories=args.exclude_directories, - exclude_filenames=args.exclude_filenames, - glob_directories=args.glob_directories, - glob_filenames=args.glob_filenames, - hash_kwargs={'bytes_per_second': args.hash_bytes_per_second}, - make_albums=args.make_albums, - new_photo_ratelimit=args.ratelimit, - recurse=args.recurse, - yield_albums=True, - yield_photos=True, - ) - for result in digest: - # print(result) - need_commit = True + with photodb.transaction: + for directory in directories: + digest = photodb.digest_directory( + directory, + exclude_directories=args.exclude_directories, + exclude_filenames=args.exclude_filenames, + glob_directories=args.glob_directories, + glob_filenames=args.glob_filenames, + hash_kwargs={'bytes_per_second': args.hash_bytes_per_second}, + make_albums=args.make_albums, + new_photo_ratelimit=args.ratelimit, + recurse=args.recurse, + yield_albums=True, + yield_photos=True, + ) + for result in digest: + # print(result) + need_commit = True - if not need_commit: - return 0 + if not need_commit: + return 0 - if args.autoyes or interactive.getpermission('Commit?'): - photodb.commit() + if not (args.autoyes or interactive.getpermission('Commit?')): + photodb.rollback() + return 1 return 0 def easybake_argparse(args): 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?'): - photodb.commit() + with photodb.transaction: + 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 @@ -310,18 +333,21 @@ def generate_thumbnail_argparse(args): photos = search_in_cwd(yield_photos=True, yield_albums=False) need_commit = False - try: - for photo in photos: - photo.generate_thumbnail() - need_commit = True - except KeyboardInterrupt: - pass - if not need_commit: - return 0 + with photodb.transaction: + try: + for photo in photos: + photo.generate_thumbnail() + need_commit = True + except KeyboardInterrupt: + pass - if args.autoyes or interactive.getpermission('Commit?'): - photodb.commit() + if not need_commit: + return 0 + + if not (args.autoyes or interactive.getpermission('Commit?')): + photodb.rollback() + return 1 return 0 @@ -335,7 +361,6 @@ def init_argparse(args): pipeable.stderr(f'PhotoDB {photodb} already exists.') return 0 photodb = etiquette.photodb.PhotoDB(create=True) - photodb.commit() return 0 def purge_deleted_files_argparse(args): @@ -348,15 +373,17 @@ def purge_deleted_files_argparse(args): need_commit = False - for deleted in photodb.purge_deleted_files(photos): - need_commit = True - print(deleted) + with photodb.transaction: + for deleted in photodb.purge_deleted_files(photos): + need_commit = True + print(deleted) - if not need_commit: - return 0 + if not need_commit: + return 0 - if args.autoyes or interactive.getpermission('Commit?'): - photodb.commit() + if not (args.autoyes or interactive.getpermission('Commit?')): + photodb.rollback() + return 1 return 0 @@ -373,25 +400,24 @@ def purge_empty_albums_argparse(args): need_commit = False - for deleted in photodb.purge_empty_albums(albums): - need_commit = True - print(deleted) + with photodb.transaction: + for deleted in photodb.purge_empty_albums(albums): + need_commit = True + print(deleted) - if not need_commit: - return 0 + if not need_commit: + return 0 - if args.autoyes or interactive.getpermission('Commit?'): - photodb.commit() + if not (args.autoyes or interactive.getpermission('Commit?')): + photodb.rollback() + return 1 return 0 def reload_metadata_argparse(args): load_photodb() - if args.any_photo_args: - photos = get_photos_from_args(args) - else: - photos = search_in_cwd(yield_photos=True, yield_albums=False) + photos = get_photos_from_args(args) hash_kwargs = { 'bytes_per_second': args.hash_bytes_per_second, @@ -399,40 +425,45 @@ def reload_metadata_argparse(args): } need_commit = False - try: - for photo in photos: - if not photo.real_path.is_file: - continue - need_reload = ( - args.force or - photo.mtime != photo.real_path.stat.st_mtime or - photo.bytes != photo.real_path.stat.st_size - ) + with photodb.transaction: + try: + for photo in photos: + if not photo.real_path.is_file: + continue - if not need_reload: - continue - photo.reload_metadata(hash_kwargs=hash_kwargs) - need_commit = True - except KeyboardInterrupt: - pass + need_reload = ( + args.force or + photo.mtime != photo.real_path.stat.st_mtime or + photo.bytes != photo.real_path.stat.st_size + ) - if not need_commit: - return 0 + if not need_reload: + continue + photo.reload_metadata(hash_kwargs=hash_kwargs) + need_commit = True + except KeyboardInterrupt: + pass - if args.autoyes or interactive.getpermission('Commit?'): - photodb.commit() + if not need_commit: + return 0 + + if not (args.autoyes or interactive.getpermission('Commit?')): + photodb.rollback() + return 1 return 0 def relocate_argparse(args): load_photodb() - photo = photodb.get_photo(args.photo_id) - photo.relocate(args.filepath) + with photodb.transaction: + photo = photodb.get_photo(args.photo_id) + photo.relocate(args.filepath) - if args.autoyes or interactive.getpermission('Commit?'): - photodb.commit() + if not (args.autoyes or interactive.getpermission('Commit?')): + photodb.rollback() + return 1 return 0 @@ -477,12 +508,14 @@ def set_unset_searchhidden_argparse(args, searchhidden): else: photos = search_in_cwd(yield_photos=True, yield_albums=False) - for photo in photos: - print(photo) - photo.set_searchhidden(searchhidden) + with photodb.transaction: + for photo in photos: + print(photo) + photo.set_searchhidden(searchhidden) - if args.autoyes or interactive.getpermission('Commit?'): - photodb.commit() + if not (args.autoyes or interactive.getpermission('Commit?')): + photodb.rollback() + return 1 return 0 @@ -514,17 +547,19 @@ def tag_breplace_argparse(args): for (tag_name, new_name, printline) in renames: print(printline) if not interactive.getpermission('Ok?', must_pick=True): - return 0 + return 1 - for (tag_name, new_name, printline) in renames: - print(printline) - tag = photodb.get_tag(tag_name) - tag.rename(new_name) - if args.set_synonym: - tag.add_synonym(tag_name) + with photodb.transaction: + for (tag_name, new_name, printline) in renames: + print(printline) + tag = photodb.get_tag(tag_name) + tag.rename(new_name) + if args.set_synonym: + tag.add_synonym(tag_name) - if args.autoyes or interactive.getpermission('Commit?'): - photodb.commit() + if not (args.autoyes or interactive.getpermission('Commit?')): + photodb.rollback() + return 1 return 0 @@ -1577,17 +1612,17 @@ def main(argv): ## 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) else: 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) else: 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 = [ photo_id for arg in args.photo_id_args @@ -1596,7 +1631,7 @@ def main(argv): else: 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 = [ album_id for arg in args.album_id_args @@ -1605,11 +1640,10 @@ def main(argv): else: args.album_id_args = None - - if not hasattr(args, 'globs'): + if not getattr(args, 'globs', None) is not None: args.globs = None - if not hasattr(args, 'glob'): + if not getattr(args, 'glob', None) is not None: args.glob = None args.any_photo_args = bool( @@ -1625,7 +1659,7 @@ def main(argv): return args try: - return betterhelp.go(parser, argv) + return betterhelp.go(parser, argv, args_postprocessor=postprocessor) except etiquette.exceptions.NoClosestPhotoDB as exc: pipeable.stderr(exc.error_message) pipeable.stderr('Try `etiquette_cli.py init` to create the database.') diff --git a/frontends/etiquette_flask/backend/endpoints/album_endpoints.py b/frontends/etiquette_flask/backend/endpoints/album_endpoints.py index 9c03e80..e99558c 100644 --- a/frontends/etiquette_flask/backend/endpoints/album_endpoints.py +++ b/frontends/etiquette_flask/backend/endpoints/album_endpoints.py @@ -58,51 +58,52 @@ def get_album_zip(album_id): @site.route('/album//add_child', methods=['POST']) @flasktools.required_fields(['child_id'], forbid_whitespace=True) 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']) - children = list(common.P_albums(child_ids, response_type='json')) - print(children) - album.add_children(children, commit=True) + with common.P.transaction: + album = common.P_album(album_id, response_type='json') + children = list(common.P_albums(child_ids, response_type='json')) + print(children) + album.add_children(children) + response = album.jsonify() return flasktools.json_response(response) @site.route('/album//remove_child', methods=['POST']) @flasktools.required_fields(['child_id'], forbid_whitespace=True) 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']) - children = list(common.P_albums(child_ids, response_type='json')) - album.remove_children(children, commit=True) + with common.P.transaction: + 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() return flasktools.json_response(response) @site.route('/album//remove_thumbnail_photo', methods=['POST']) def post_album_remove_thumbnail_photo(album_id): - album = common.P_album(album_id, response_type='json') - album.set_thumbnail_photo(None) - common.P.commit(message='album remove thumbnail photo endpoint') + with common.P.transaction: + album = common.P_album(album_id, response_type='json') + album.set_thumbnail_photo(None) return flasktools.json_response(album.jsonify()) @site.route('/album//refresh_directories', methods=['POST']) def post_album_refresh_directories(album_id): - album = common.P_album(album_id, response_type='json') - for directory in album.get_associated_directories(): - if not directory.is_dir: - continue - digest = common.P.digest_directory(directory, new_photo_ratelimit=0.1) - gentools.run(digest) - common.P.commit(message='refresh album directories endpoint') + with common.P.transaction: + album = common.P_album(album_id, response_type='json') + for directory in album.get_associated_directories(): + if not directory.is_dir: + continue + digest = common.P.digest_directory(directory, new_photo_ratelimit=0.1) + gentools.run(digest) return flasktools.json_response({}) @site.route('/album//set_thumbnail_photo', methods=['POST']) @flasktools.required_fields(['photo_id'], forbid_whitespace=True) def post_album_set_thumbnail_photo(album_id): - album = common.P_album(album_id, response_type='json') - photo = common.P_photo(request.form['photo_id'], response_type='json') - album.set_thumbnail_photo(photo) - common.P.commit(message='album set thumbnail photo endpoint') + with common.P.transaction: + album = common.P_album(album_id, response_type='json') + photo = common.P_photo(request.form['photo_id'], response_type='json') + album.set_thumbnail_photo(photo) return flasktools.json_response(album.jsonify()) # Album photo operations ########################################################################### @@ -113,11 +114,12 @@ def post_album_add_photo(album_id): ''' 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']) - photos = list(common.P_photos(photo_ids, response_type='json')) - album.add_photos(photos, commit=True) + with common.P.transaction: + 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() return flasktools.json_response(response) @@ -127,11 +129,11 @@ def post_album_remove_photo(album_id): ''' 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']) - photos = list(common.P_photos(photo_ids, response_type='json')) - album.remove_photos(photos, commit=True) + with common.P.transaction: + 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() 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. ''' 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() - try: - tag = common.P_tag(tag, response_type='json') - except etiquette.exceptions.NoSuchTag as exc: - response = exc.jsonify() - return flasktools.json_response(response, status=404) - recursive = request.form.get('recursive', False) - recursive = stringtools.truthystring(recursive) - album.add_tag_to_all(tag, nested_children=recursive, commit=True) + tag = request.form['tagname'].strip() + try: + tag = common.P_tag(tag, response_type='json') + except etiquette.exceptions.NoSuchTag as exc: + response = exc.jsonify() + return flasktools.json_response(response, status=404) + recursive = request.form.get('recursive', False) + recursive = stringtools.truthystring(recursive) + album.add_tag_to_all(tag, nested_children=recursive) response['action'] = 'add_tag' response['tagname'] = tag.name return flasktools.json_response(response) @@ -165,11 +168,13 @@ def post_album_edit(album_id): ''' Edit the title / description. ''' - album = common.P_album(album_id, response_type='json') - title = request.form.get('title', 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) return flasktools.json_response(response) @@ -234,16 +239,17 @@ def post_albums_create(): user = session_manager.get(request).user - album = common.P.new_album(title=title, description=description, author=user) - if parent_id is not None: - parent.add_child(album) - common.P.commit('create album endpoint') + with common.P.transaction: + album = common.P.new_album(title=title, description=description, author=user) + if parent_id is not None: + parent.add_child(album) response = album.jsonify(minimal=False) return flasktools.json_response(response) @site.route('/album//delete', methods=['POST']) def post_album_delete(album_id): - album = common.P_album(album_id, response_type='json') - album.delete(commit=True) + with common.P.transaction: + album = common.P_album(album_id, response_type='json') + album.delete() return flasktools.json_response({}) diff --git a/frontends/etiquette_flask/backend/endpoints/bookmark_endpoints.py b/frontends/etiquette_flask/backend/endpoints/bookmark_endpoints.py index 6cfd8f3..a4c2876 100644 --- a/frontends/etiquette_flask/backend/endpoints/bookmark_endpoints.py +++ b/frontends/etiquette_flask/backend/endpoints/bookmark_endpoints.py @@ -19,11 +19,12 @@ def get_bookmark_json(bookmark_id): @site.route('/bookmark//edit', methods=['POST']) def post_bookmark_edit(bookmark_id): - bookmark = common.P_bookmark(bookmark_id, response_type='json') - # Emptystring is okay for titles, but not for URL. - title = request.form.get('title', None) - url = request.form.get('url', None) or None - bookmark.edit(title=title, url=url, commit=True) + with common.P.transaction: + bookmark = common.P_bookmark(bookmark_id, response_type='json') + # Emptystring is okay for titles, but not for URL. + title = request.form.get('title', None) + url = request.form.get('url', None) or None + bookmark.edit(title=title, url=url) response = bookmark.jsonify() response = flasktools.json_response(response) @@ -49,13 +50,15 @@ def post_bookmark_create(): url = request.form['url'] title = request.form.get('title', None) 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 = flasktools.json_response(response) return response @site.route('/bookmark//delete', methods=['POST']) def post_bookmark_delete(bookmark_id): - bookmark = common.P_bookmark(bookmark_id, response_type='json') - bookmark.delete(commit=True) + with common.P.transaction: + bookmark = common.P_bookmark(bookmark_id, response_type='json') + bookmark.delete() return flasktools.json_response({}) diff --git a/frontends/etiquette_flask/backend/endpoints/photo_endpoints.py b/frontends/etiquette_flask/backend/endpoints/photo_endpoints.py index 6f28fc4..cbe29da 100644 --- a/frontends/etiquette_flask/backend/endpoints/photo_endpoints.py +++ b/frontends/etiquette_flask/backend/endpoints/photo_endpoints.py @@ -75,11 +75,11 @@ def get_thumbnail(photo_id): @site.route('/photo//delete', methods=['POST']) 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 = 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({}) # 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')) tag = common.P_tag(tagname, response_type='json') - for photo in photos: - if add_or_remove == 'add': - photo.add_tag(tag) - elif add_or_remove == 'remove': - photo.remove_tag(tag) - common.P.commit('photo add remove tag core') + with common.P.transaction: + for photo in photos: + if add_or_remove == 'add': + photo.add_tag(tag) + elif add_or_remove == 'remove': + photo.remove_tag(tag) response = {'action': add_or_remove, 'tagname': tag.name} return flasktools.json_response(response) @@ -120,10 +120,10 @@ def post_photo_copy_tags(photo_id): ''' Copy the tags from another photo. ''' - photo = common.P_photo(photo_id, response_type='json') - other = common.P_photo(request.form['other_photo'], response_type='json') - photo.copy_tags(other) - common.P.commit('photo copy tags') + with common.P.transaction: + photo = common.P_photo(photo_id, response_type='json') + other = common.P_photo(request.form['other_photo'], response_type='json') + photo.copy_tags(other) return flasktools.json_response([tag.jsonify(minimal=True) for tag in photo.get_tags()]) @site.route('/photo//remove_tag', methods=['POST']) @@ -164,10 +164,10 @@ def post_batch_photos_remove_tag(): @site.route('/photo//generate_thumbnail', methods=['POST']) def post_photo_generate_thumbnail(photo_id): special = request.form.to_dict() - special.pop('commit', None) - photo = common.P_photo(photo_id, response_type='json') - photo.generate_thumbnail(commit=True, **special) + with common.P.transaction: + photo = common.P_photo(photo_id, response_type='json') + photo.generate_thumbnail(**special) response = flasktools.json_response({}) return response @@ -176,22 +176,21 @@ def post_photo_refresh_metadata_core(photo_ids): if isinstance(photo_ids, str): 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: - photo._uncache() - photo = common.P_photo(photo.id, response_type='json') - try: - photo.reload_metadata() - except pathclass.NotFile: - flask.abort(404) - if photo.thumbnail is None: + for photo in photos: + photo._uncache() + photo = common.P_photo(photo.id, response_type='json') try: - photo.generate_thumbnail() - except Exception: - log.warning(traceback.format_exc()) - - common.P.commit('photo refresh metadata core') + photo.reload_metadata() + except pathclass.NotFile: + flask.abort(404) + if photo.thumbnail is None: + try: + photo.generate_thumbnail() + except Exception: + log.warning(traceback.format_exc()) return flasktools.json_response({}) @@ -208,26 +207,27 @@ def post_batch_photos_refresh_metadata(): @site.route('/photo//set_searchhidden', methods=['POST']) def post_photo_set_searchhidden(photo_id): - photo = common.P_photo(photo_id, response_type='json') - photo.set_searchhidden(True) + with common.P.transaction: + photo = common.P_photo(photo_id, response_type='json') + photo.set_searchhidden(True) return flasktools.json_response({}) @site.route('/photo//unset_searchhidden', methods=['POST']) def post_photo_unset_searchhidden(photo_id): - photo = common.P_photo(photo_id, response_type='json') - photo.set_searchhidden(False) + with common.P.transaction: + photo = common.P_photo(photo_id, response_type='json') + photo.set_searchhidden(False) return flasktools.json_response({}) def post_batch_photos_searchhidden_core(photo_ids, searchhidden): if isinstance(photo_ids, str): 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: - photo.set_searchhidden(searchhidden) - - common.P.commit('photo set searchhidden core') + for photo in photos: + photo.set_searchhidden(searchhidden) return flasktools.json_response({}) diff --git a/frontends/etiquette_flask/backend/endpoints/tag_endpoints.py b/frontends/etiquette_flask/backend/endpoints/tag_endpoints.py index e92a259..e73b7ed 100644 --- a/frontends/etiquette_flask/backend/endpoints/tag_endpoints.py +++ b/frontends/etiquette_flask/backend/endpoints/tag_endpoints.py @@ -44,24 +44,25 @@ def get_tag_json(specific_tag_name): @site.route('/tag//edit', methods=['POST']) def post_tag_edit(tagname): - tag = common.P_tag(tagname, response_type='json') - name = request.form.get('name', '').strip() - if name: - tag.rename(name) + with common.P.transaction: + tag = common.P_tag(tagname, response_type='json') + name = request.form.get('name', '').strip() + if name: + tag.rename(name) - description = request.form.get('description', None) - tag.edit(description=description, commit=True) + description = request.form.get('description', None) + tag.edit(description=description) - response = tag.jsonify() - response = flasktools.json_response(response) + response = flasktools.json_response(tag.jsonify()) return response @site.route('/tag//add_child', methods=['POST']) @flasktools.required_fields(['child_name'], forbid_whitespace=True) def post_tag_add_child(tagname): - parent = common.P_tag(tagname, response_type='json') - child = common.P_tag(request.form['child_name'], response_type='json') - parent.add_child(child, commit=True) + with common.P.transaction: + parent = common.P_tag(tagname, response_type='json') + 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}'} return flasktools.json_response(response) @@ -70,8 +71,9 @@ def post_tag_add_child(tagname): def post_tag_add_synonym(tagname): syn_name = request.form['syn_name'] - master_tag = common.P_tag(tagname, response_type='json') - syn_name = master_tag.add_synonym(syn_name, commit=True) + with common.P.transaction: + master_tag = common.P_tag(tagname, response_type='json') + syn_name = master_tag.add_synonym(syn_name) response = {'action': 'add_synonym', 'synonym': syn_name} return flasktools.json_response(response) @@ -79,9 +81,10 @@ def post_tag_add_synonym(tagname): @site.route('/tag//remove_child', methods=['POST']) @flasktools.required_fields(['child_name'], forbid_whitespace=True) def post_tag_remove_child(tagname): - parent = common.P_tag(tagname, response_type='json') - child = common.P_tag(request.form['child_name'], response_type='json') - parent.remove_child(child, commit=True) + with common.P.transaction: + parent = common.P_tag(tagname, response_type='json') + 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}'} return flasktools.json_response(response) @@ -90,8 +93,9 @@ def post_tag_remove_child(tagname): def post_tag_remove_synonym(tagname): syn_name = request.form['syn_name'] - master_tag = common.P_tag(tagname, response_type='json') - syn_name = master_tag.remove_synonym(syn_name, commit=True) + with common.P.transaction: + master_tag = common.P_tag(tagname, response_type='json') + syn_name = master_tag.remove_synonym(syn_name) response = {'action': 'delete_synonym', 'synonym': syn_name} return flasktools.json_response(response) @@ -163,7 +167,8 @@ def post_tag_create(): name = request.form['name'] 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() return flasktools.json_response(response) @@ -172,13 +177,15 @@ def post_tag_create(): def post_tag_easybake(): 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] return flasktools.json_response(notes) @site.route('/tag//delete', methods=['POST']) def post_tag_delete(tagname): - tag = common.P_tag(tagname, response_type='json') - tag.delete(commit=True) + with common.P.transaction: + tag = common.P_tag(tagname, response_type='json') + tag.delete() response = {'action': 'delete_tag', 'tagname': tag.name} return flasktools.json_response(response) diff --git a/frontends/etiquette_flask/backend/endpoints/user_endpoints.py b/frontends/etiquette_flask/backend/endpoints/user_endpoints.py index 20e5d40..69282d0 100644 --- a/frontends/etiquette_flask/backend/endpoints/user_endpoints.py +++ b/frontends/etiquette_flask/backend/endpoints/user_endpoints.py @@ -47,9 +47,8 @@ def post_user_edit(username): display_name = request.form.get('display_name') if display_name is not None: - user.set_display_name(display_name) - - common.P.commit() + with common.P.transaction: + user.set_display_name(display_name) return flasktools.json_response(user.jsonify()) @@ -127,7 +126,8 @@ def post_register(): } 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_manager.add(session) diff --git a/utilities/database_upgrader.py b/utilities/database_upgrader.py index 6d73f11..7559088 100644 --- a/utilities/database_upgrader.py +++ b/utilities/database_upgrader.py @@ -2,8 +2,12 @@ import argparse import os import sys +from voussoirkit import vlogging + import etiquette +log = vlogging.get_logger(__name__, 'database_upgrader') + class Migrator: ''' 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, # which is about to get renamed to B_old and then A's reference will be # broken. - self.photodb.execute('PRAGMA foreign_keys = OFF') - self.photodb.execute('BEGIN') + self.photodb.pragma_write('foreign_keys', 'OFF') for (name, table) in self.tables.items(): if name not in self.existing_tables: continue @@ -65,16 +68,14 @@ class Migrator: for (name, query) in self.indices: self.photodb.execute(query) + self.photodb.pragma_write('foreign_keys', 'ON') def upgrade_1_to_2(photodb): ''' 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). ''' - photodb.executescript(''' - BEGIN; - ALTER TABLE photos ADD COLUMN tagged_at INT; - ''') + photodb.execute('ALTER TABLE photos ADD COLUMN tagged_at INT') def upgrade_2_to_3(photodb): ''' @@ -83,8 +84,6 @@ def upgrade_2_to_3(photodb): Plus some indices. ''' photodb.executescript(''' - BEGIN; - CREATE TABLE users( id TEXT, username TEXT COLLATE NOCASE, @@ -102,8 +101,6 @@ def upgrade_3_to_4(photodb): Add an `author_id` column to Photos. ''' photodb.executescript(''' - BEGIN; - ALTER TABLE photos ADD COLUMN author_id TEXT; 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. ''' photodb.executescript(''' - BEGIN; - CREATE TABLE bookmarks( id TEXT, title TEXT, @@ -259,18 +254,13 @@ def upgrade_7_to_8(photodb): ''' Give the Tags table a description field. ''' - photodb.executescript(''' - BEGIN; - ALTER TABLE tags ADD COLUMN description TEXT; - ''') + photodb.executescript('ALTER TABLE tags ADD COLUMN description TEXT') def upgrade_8_to_9(photodb): ''' Give the Photos table a searchhidden field. ''' photodb.executescript(''' - BEGIN; - ALTER TABLE photos ADD COLUMN searchhidden INT; 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 new intersection-based search. ''' - photodb.executescript(''' - BEGIN; - + photodb.execute(''' 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) - current_version = photodb.execute('PRAGMA user_version').fetchone()[0] + current_version = photodb.pragma_read('user_version') needed_version = etiquette.constants.DATABASE_VERSION if current_version == needed_version: print('Already up to date with version %d.' % needed_version) + photodb.close() return 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 = eval(upgrade_function) - try: - photodb.execute('PRAGMA foreign_keys = ON') + photodb.pragma_write('journal_mode', 'wal') + with photodb.transaction: + photodb.pragma_write('foreign_keys', 'ON') upgrade_function(photodb) - except Exception as exc: - photodb.rollback() - raise - else: - photodb.sql.cursor().execute('PRAGMA user_version = %d' % version_number) - photodb.commit() + photodb.pragma_write('user_version', version_number) current_version = version_number + photodb.close() print('Upgrades finished.') def upgrade_all_argparse(args): return upgrade_all(data_directory=args.data_directory) +@vlogging.main_decorator def main(argv): parser = argparse.ArgumentParser()