# included libs
from typing import Any, Callable
from json import loads as json_loads
-from urllib.request import urlopen
+from urllib.request import Request, urlopen
# non-included libs
from paramiko import SSHClient # type: ignore
from scp import SCPClient # type: ignore
for yt_video_local in YoutubeVideo.get_all(db_local):
back_and_forth(sync_relations, (db_local, db_remote),
yt_video_local)
- db_remote.commit()
- db_local.commit()
+ for db in (db_remote, db_local):
+ db.commit()
scp.put(PATH_DB_REMOTE, PATH_DB)
PATH_DB_REMOTE.unlink()
+def _urls_here_and_there(config: Config, page_name: str) -> tuple[str, ...]:
+ return tuple(f'http://{host}:{port}/{PAGE_NAMES[page_name]}'
+ for host, port in ((config.remote, config.port_remote),
+ (config.host, config.port)))
+
+
+def purge_deleteds(config: Config) -> None:
+ """Command both servers to actually purge "deleted" files."""
+ for url_purge in _urls_here_and_there(config, 'purge'):
+ with urlopen(Request(url_purge, method='POST')) as response:
+ print(f'SYNC: Calling purge via {url_purge} – {response.read()}')
+
+
def fill_missing(scp: SCPClient, config: Config) -> None:
"""Between config.host and .remote, fill files listed in as missing."""
missings = []
- for host, port in ((config.remote, config.port_remote),
- (config.host, config.port)):
- url_missing = f'http://{host}:{port}/{PAGE_NAMES["missing"]}'
+ for url_missing in _urls_here_and_there(config, 'missing'):
with urlopen(url_missing) as response:
missings += [list(json_loads(response.read()))]
conn = DbConn()
ssh.connect(config.remote)
with SCPClient(ssh.get_transport()) as scp:
sync_dbs(scp)
+ purge_deleteds(config)
fill_missing(scp, config)
'missing': Path('missing'),
'player': Path('player'),
'playlist': Path('playlist'),
+ 'purge': Path('purge'),
'thumbnails': Path('thumbnails'),
'yt_result': Path('yt_result'),
'yt_query': Path('yt_query'),
self._receive_yt_query(QueryText(postvars.first_for('query')))
elif PAGE_NAMES['player'] == page_name:
self._receive_player_command(postvars)
+ elif PAGE_NAMES['purge'] == page_name:
+ self._purge_deleted_files()
+
+ def _purge_deleted_files(self) -> None:
+ with DbConn() as conn:
+ VideoFile.purge_deleteds(conn)
+ conn.commit()
+ self._send_http('OK', code=200)
def _receive_player_command(self, postvars: _ReqMap) -> None:
command = postvars.first_for('command')
def ensure_absence_if_deleted(self) -> None:
"""If 'delete' flag set, ensure no actual file in filesystem."""
if self.is_flag_set(FlagName('delete')) and self.present:
- print(f'SYNC: {self.rel_path} set "delete", '
- 'removing from filesystem.')
self.unlink_locally()
def unlink_locally(self) -> None:
"""Remove actual file from local filesystem."""
+ print(f'SYNC: removing from filesystem: {self.rel_path}')
self.full_path.unlink()
+ @classmethod
+ def purge_deleteds(cls, conn: BaseDbConn) -> None:
+ """For all of .is_flag_set("deleted"), remove file _and_ DB entry."""
+ for file in [f for f in cls.get_all(conn)
+ if f.is_flag_set(FlagName('delete'))]:
+ if file.present:
+ file.unlink_locally()
+ print(f'SYNC: purging off DB: {file.digest.b64} ({file.rel_path})')
+ conn.exec(
+ SqlText(f'DELETE FROM {cls._table_name} WHERE digest = ?'),
+ (file.digest.bytes,))
+
class QuotaLog(DbData):
"""Collects API access quota costs."""