diff --git a/src/crystal/model.py b/src/crystal/model.py index 32fd2cee..e5d7cde9 100644 --- a/src/crystal/model.py +++ b/src/crystal/model.py @@ -38,6 +38,7 @@ get_column_names_of_table, get_index_names, is_no_such_column_error_for, + is_no_such_table_error_for, ) from crystal.util.ellipsis import Ellipsis, EllipsisType from crystal.util import gio @@ -54,7 +55,7 @@ from crystal.util.xgc import gc_disabled from crystal.util.xos import is_linux, is_mac_os, is_windows from crystal.util import xshutil -from crystal.util.xsqlite3 import sqlite_has_json_support +from crystal.util.xsqlite3 import is_database_closed_error, sqlite_has_json_support from crystal.util.xthreading import ( bg_affinity, bg_call_later, fg_affinity, fg_call_and_wait, fg_call_later, is_foreground_thread, @@ -2461,7 +2462,18 @@ def fg_task() -> None: suffix='.body', dir=os.path.join(project.path, Project._TEMPORARY_DIRNAME), delete=False) as body_file: - xshutil.copyfileobj_readinto(body_stream, body_file) + try: + xshutil.copyfileobj_readinto(body_stream, body_file) + finally: + try: + ResourceRevision._log_bytes_downloaded( + body_file.tell(), + resource.project) + except sqlite3.ProgrammingError as e: + if is_database_closed_error(e): + pass + else: + raise body_file_downloaded_ok = True else: body_file = None @@ -3138,6 +3150,43 @@ def delete(self): self.resource.already_downloaded_this_session = False + # === Logging === + + @staticmethod + def _log_bytes_downloaded(num_bytes_downloaded: int, project: Project) -> None: + """ + Logs that the specified number of bytes were downloaded, + if the following statistics table exists: + + create table statistics (date text primary key asc, num_bytes_downloaded int default 0) + """ + def fg_task() -> None: + today_local_tz = datetime.date.today() # capture + + try: + c = project._db.cursor() + c.execute( + 'insert into statistics ' + '(date, num_bytes_downloaded) ' + 'values (?, ?) ' + 'on conflict (date) do ' + 'update ' + 'set num_bytes_downloaded = num_bytes_downloaded + ?', + (str(today_local_tz), num_bytes_downloaded, num_bytes_downloaded)) + project._db.commit() + except sqlite3.OperationalError as e: + if is_no_such_table_error_for('statistics', e): + # Fail silently if statistics table does not exist + pass + else: + raise + except ProjectClosedError: + # Fail silently if project is closed + pass + fg_call_later(fg_task) + + # === Utility === + def __repr__(self) -> str: return "" % (self._id, self.resource.url) diff --git a/src/crystal/util/db.py b/src/crystal/util/db.py index c6882add..ed12fb04 100644 --- a/src/crystal/util/db.py +++ b/src/crystal/util/db.py @@ -82,6 +82,13 @@ def is_no_such_column_error_for(column_name: str, e: Exception) -> bool: ) +def is_no_such_table_error_for(table_name: str, e: Exception) -> bool: + return ( + isinstance(e, sqlite3.OperationalError) and + str(e) == f'no such table: {table_name}' + ) + + def get_index_names(c: DatabaseCursor) -> list[str]: return [ index_name