fix problem when deleting MediaFile with MediaActor relations

This commit is contained in:
2026-05-31 17:47:27 +02:00
parent 9cb71f18c2
commit 2d706cc3d8
8 changed files with 141 additions and 67 deletions
+9 -3
View File
@@ -2,7 +2,10 @@ from typing import List
from fastapi import APIRouter, status, HTTPException from fastapi import APIRouter, status, HTTPException
from src.core.log_conf import logger from src.core.log_conf import logger
from src.db.repository.media.actorfile import create_new_mediaactorfile from src.db.repository.media.actorfile import (
create_new_mediaactorfile,
delete_mediaactorfile,
)
from src.db.repository.media.file import delete_mediafile, import_mediafile from src.db.repository.media.file import delete_mediafile, import_mediafile
from src.db.session import SessionDep from src.db.session import SessionDep
from src.schema.media.actor import MediaActorResponse, actor_to_response from src.schema.media.actor import MediaActorResponse, actor_to_response
@@ -78,8 +81,11 @@ def delete_file(file_id: str, db: SessionDep):
logger.info("delete MediaFile: %s", file_id) logger.info("delete MediaFile: %s", file_id)
actor_files = mediafile.media_actor_files actor_files = mediafile.media_actor_files
logger.info("MediaActorFiles links %s", len(actor_files)) logger.info("MediaActorFiles links %s", len(actor_files))
if len(actor_files) == 0: if len(actor_files) > 0:
delete_mediafile(db, mediafile.id) logger.info("delete MediaActor relations first")
for actor_file in actor_files:
delete_mediaactorfile(db, actor_file.id)
delete_mediafile(db, mediafile.id)
@router.get("/files/{file_id}/actors", response_model=list[MediaActorResponse]) @router.get("/files/{file_id}/actors", response_model=list[MediaActorResponse])
@@ -5,12 +5,16 @@ from sqlalchemy.orm import Session
from src.core.log_conf import logger from src.core.log_conf import logger
from src.db.models.media import MediaActorFile from src.db.models.media import MediaActorFile
from src.schema.media.actor import MediaActorModel
from src.schema.media.actorfile import MediaActorFileModel from src.schema.media.actorfile import MediaActorFileModel
def create_new_mediaactorfile(db: Session, actor_id: str, file_id: str) -> MediaActorFile: def create_new_mediaactorfile(
logger.info(f"create MediaActorFile with actor {actor_id} and file {file_id}") db: Session, actor_id: str, file_id: str
) -> MediaActorFile:
"""
Create relation for MediaFile and MediaActor
"""
logger.info("create MediaActorFile with actor %s and file %s", actor_id, file_id)
media_actor_file: MediaActorFile = MediaActorFile() media_actor_file: MediaActorFile = MediaActorFile()
media_actor_file.id = str(uuid.uuid4()) media_actor_file.id = str(uuid.uuid4())
media_actor_file.created_date = datetime.now() media_actor_file.created_date = datetime.now()
@@ -23,17 +27,23 @@ def create_new_mediaactorfile(db: Session, actor_id: str, file_id: str) -> Media
db.refresh(media_actor_file) db.refresh(media_actor_file)
return media_actor_file return media_actor_file
def delete_mediaactorfile(db: Session, actorfile_id: str): def delete_mediaactorfile(db: Session, actorfile_id: str):
logger.info(f"delete MediaActorFile with id {actorfile_id}") """
Delete relation between MediaFile and MediaActor.
"""
logger.info("delete MediaActorFile with id %s", actorfile_id)
media_actorfile = db.get(MediaActorFile, actorfile_id) media_actorfile = db.get(MediaActorFile, actorfile_id)
db.delete(media_actorfile) db.delete(media_actorfile)
db.commit() db.commit()
def import_mediaactorfile(db: Session, new_actorfile: MediaActorFileModel) -> MediaActorFile:
def import_mediaactorfile(
db: Session, new_actorfile: MediaActorFileModel
) -> MediaActorFile:
""" """
Import MediaFile and set missing values with default ones. Import MediaFile and set missing values with default ones.
""" """
logger.info("import MediaActorFile with %s", new_actorfile) logger.info("import MediaActorFile with %s", new_actorfile)
media_actor_file: MediaActorFile = MediaActorFile() media_actor_file: MediaActorFile = MediaActorFile()
return media_actor_file return media_actor_file
+10 -2
View File
@@ -9,6 +9,9 @@ from src.schema.media.file import MediaFileModel
def create_new_mediafile(link: str, db: Session) -> MediaFile: def create_new_mediafile(link: str, db: Session) -> MediaFile:
"""
Create MediaFile with gievne URL.
"""
logger.info("create MediaFile with url {link}") logger.info("create MediaFile with url {link}")
media_file: MediaFile = MediaFile() media_file: MediaFile = MediaFile()
media_file.id = str(uuid.uuid4()) media_file.id = str(uuid.uuid4())
@@ -21,15 +24,20 @@ def create_new_mediafile(link: str, db: Session) -> MediaFile:
db.add(media_file) db.add(media_file)
db.commit() db.commit()
db.refresh(media_file) db.refresh(media_file)
logger.info(f"created {media_file}") logger.info("created %s", media_file)
return media_file return media_file
def delete_mediafile(db: Session, media_file_id: str): def delete_mediafile(db: Session, media_file_id: str):
logger.info(f"delete MediaFile with id {media_file_id}") """
Delete MediaFile with given ID from db.
"""
logger.info("delete MediaFile with id %s", media_file_id)
media_file = db.get(MediaFile, media_file_id) media_file = db.get(MediaFile, media_file_id)
db.delete(media_file) db.delete(media_file)
db.commit() db.commit()
def import_mediafile(db: Session, new_file: MediaFileModel) -> MediaFile: def import_mediafile(db: Session, new_file: MediaFileModel) -> MediaFile:
""" """
import MediaActor and set missing values with defautl ones. import MediaActor and set missing values with defautl ones.
+12 -2
View File
@@ -114,7 +114,7 @@ class Server:
password: str password: str
timeout: int timeout: int
def login(self, log: Logger, refresh_token: bool= False): def login(self, log: Logger, refresh_token: bool = False):
""" """
get token from server by calling login endpoint. get token from server by calling login endpoint.
""" """
@@ -184,7 +184,9 @@ class Server:
""" """
url: str = f"{self.url}/{MAPPING[table]}" url: str = f"{self.url}/{MAPPING[table]}"
headers: Dict[str, str] = {"Authorization": f"Bearer {self.token}"} headers: Dict[str, str] = {"Authorization": f"Bearer {self.token}"}
create = requests.post(url, headers=headers, json=new_item, timeout=self.timeout) create = requests.post(
url, headers=headers, json=new_item, timeout=self.timeout
)
log.info(f"Status: {create.status_code}") log.info(f"Status: {create.status_code}")
if create.status_code == 404: if create.status_code == 404:
raise EndPointNotAvailableException raise EndPointNotAvailableException
@@ -193,6 +195,14 @@ class Server:
data = create.json() data = create.json()
return data return data
def delete(self, log: Logger, table: str, item_id: str):
"""
Delete item in Kontor-API instance.
"""
url: str = f"{self.url}/{MAPPING[table]}/{item_id}"
headers: Dict[str, str] = {"Authorization": f"Bearer {self.token}"}
delete = requests.delete(url, headers=headers, timeout=self.timeout)
log.info(f"Status: {delete.status_code}")
@dataclass @dataclass
+65 -44
View File
@@ -1,40 +1,27 @@
""" """
Checks the database kontor Checks the database kontor
""" """
from dataclasses import dataclass
from enum import Enum, auto
from logging import Logger from logging import Logger
from typing import Dict, List, Optional from pathlib import Path
from typing import Any, Dict, List, Optional
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
from urllib.parse import urlparse from urllib.parse import urlparse
import click
from simple_term_menu import TerminalMenu
from api import Option, OptionType, Server, get_api_config, get_logger from api import Server, get_api_config, get_logger
parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument("--verbose", "-v", action="count", default=0) parser.add_argument("--verbose", "-v", action="count", default=0)
parser.add_argument("--config", "-c", default="kontor-api") parser.add_argument("--config", "-c", default="kontor-api")
parser.add_argument("--dir", "-d", default="/data/media") parser.add_argument("--dir", "-d", default="/data/media")
parser.add_argument("--add-dir", "-a", action="append")
parser.add_argument("--dry-run", "-m", action="store_true") parser.add_argument("--dry-run", "-m", action="store_true")
parser.add_argument("--server", "-s") parser.add_argument("--server", "-s")
args = parser.parse_args() args = parser.parse_args()
class StatusType(Enum):
UNKNOWN = auto()
FILE_NAME = auto()
FILE_ID = auto()
DUPLICATE = auto()
CLOUD_LINK = auto()
CLOUD_LINK_ID = auto()
class FileStatus:
id: str | None = None
status_type: StatusType = StatusType.UNKNOWN
def get_response(self, response: dict):
self.status_type = StatusType.FILE_NAME
self.id = response['id']
def create_item_id_mapping(log: Logger, data_list: List[dict]) -> Dict[str, dict]: def create_item_id_mapping(log: Logger, data_list: List[dict]) -> Dict[str, dict]:
""" """
@@ -47,8 +34,32 @@ def create_item_id_mapping(log: Logger, data_list: List[dict]) -> Dict[str, dict
return item_id_mapping return item_id_mapping
def check_duplicate_links(log: Logger, server: Server): def remove_file(log: Logger, item_data: Dict[str, Any], media_dirs: List[str]):
data = server.request(log=logger, table="media_file") """
Delete file from path in dictionary.
"""
log.debug(item_data)
cloud_link = item_data["cloud_link"]
for file_dir in media_dirs:
log.info("look in %s", file_dir)
file_name = Path(cloud_link).name
media_file = Path(file_dir, file_name)
if media_file.exists():
log.info("File to remove %s", media_file.absolute())
media_file.unlink(missing_ok=True)
break
else:
log.info("File not found %s", media_file.absolute())
def check_duplicate_links(log: Logger, server: Optional[Server], media_dirs: List[str]):
"""
Check if there are MediaFile URLs which only differ in hostname.
"""
if server is None:
log.info("no server selected")
return
data = server.request(log=log, table="media_file")
mapping = create_item_id_mapping(log=log, data_list=data) mapping = create_item_id_mapping(log=log, data_list=data)
visited_link_path: Dict[str, str] = {} visited_link_path: Dict[str, str] = {}
duplicate_link_paths: Dict[str, List[str]] = {} duplicate_link_paths: Dict[str, List[str]] = {}
@@ -60,7 +71,7 @@ def check_duplicate_links(log: Logger, server: Server):
parsed_url = urlparse(link) parsed_url = urlparse(link)
link_path = parsed_url.path link_path = parsed_url.path
if link_path in visited_link_path: if link_path in visited_link_path:
log.info("duplicate url path found: %s", link_path) log.debug("duplicate url path found: %s", link_path)
if link_path in duplicate_link_paths: if link_path in duplicate_link_paths:
duplicate_link_paths[link_path].append(file_id) duplicate_link_paths[link_path].append(file_id)
else: else:
@@ -70,32 +81,42 @@ def check_duplicate_links(log: Logger, server: Server):
else: else:
visited_link_path[link_path] = file_id visited_link_path[link_path] = file_id
log.info("found %s duplicate links", len(duplicate_link_paths.keys())) log.info("found %s duplicate links", len(duplicate_link_paths.keys()))
deletion_list: List[str] = [] for _, value in duplicate_link_paths.items():
for key, value in duplicate_link_paths.items(): choices = [mapping[value[0]]["url"], mapping[value[1]]["url"], "Abbruch"]
if len(value) == 2: menu = TerminalMenu(
log.info("%s:\n%s - %s\n%s - %s", key, value[0], mapping[value[0]]["url"], value[1], mapping[value[1]]["url"]) choices, title="Choose an link to delete:", multi_select=False
if mapping[value[0]]["url"].startswith("https://xhamster"): )
deletion_list.append(value[0]) menu_choice = menu.show()
else: if isinstance(menu_choice, int):
deletion_list.append(value[1]) if menu_choice == 2:
break
index: int = int(menu_choice)
server.delete(log=log, table="media_file", item_id=value[index])
remove_file(log, mapping[value[index]], media_dirs)
else: else:
log.info("found %s links", len(value)) print("selection canceled")
for key in deletion_list:
log.info("%s - %s", key, mapping[key]["url"])
if __name__ == '__main__': if __name__ == "__main__":
logger = get_logger(args.verbose, args.config) logger = get_logger(args.verbose, args.config)
logger.info("kontor.check_kontor started") logger.info("kontor.check_kontor started")
APICONFIG = get_api_config(logger, args.config) APICONFIG = get_api_config(logger, args.config)
server: Server = APICONFIG.server[0] first_server: Optional[Server] = APICONFIG.get_server(args.server)
if not first_server:
SystemExit(2)
dirs: List[str] = args.add_dir
if dirs is None:
dirs = [args.dir]
else:
dirs.insert(0, args.dir)
logger.info(dirs)
logger.info("kontor.check_kontor.check_duplicate_links") logger.info("kontor.check_kontor.check_duplicate_links")
check_duplicate_links(logger, server) check_duplicate_links(logger, first_server, dirs)
#logger.info("kontor.check_kontor.update_cloud_link_with_found_files") # logger.info("kontor.check_kontor.update_cloud_link_with_found_files")
#update_cloud_link_with_found_files(data_dir, mariadb_conn, args.dry_run) # update_cloud_link_with_found_files(data_dir, mariadb_conn, args.dry_run)
#logger.info("kontor.check_kontor.get_ids_from_column_cloud_link") # logger.info("kontor.check_kontor.get_ids_from_column_cloud_link")
#get_ids_from_column_cloud_link(link_list, mariadb_cursor) # get_ids_from_column_cloud_link(link_list, mariadb_cursor)
#logger.info('found {} ids in column cloud_link'.format(len(link_list))) # logger.info('found {} ids in column cloud_link'.format(len(link_list)))
#logger.info("kontor.check_kontor.checking_ids_from_cloud_link") # logger.info("kontor.check_kontor.checking_ids_from_cloud_link")
#checking_ids_from_cloud_link(link_list, mariadb_cursor) # checking_ids_from_cloud_link(link_list, mariadb_cursor)
logger.info("kontor.check_kontor finished") logger.info("kontor.check_kontor finished")
+2
View File
@@ -11,6 +11,7 @@ maintainers = [
] ]
dependencies = [ dependencies = [
"beautifulsoup4>=4.13.4", "beautifulsoup4>=4.13.4",
"click>=8.1.8",
"coverage>=7.8.0", "coverage>=7.8.0",
"fastapi[standard]>=0.115.12", "fastapi[standard]>=0.115.12",
"pathlib>=1.0.1", "pathlib>=1.0.1",
@@ -19,6 +20,7 @@ dependencies = [
"pytest-cov>=6.1.1", "pytest-cov>=6.1.1",
"pyyaml>=6.0.2", "pyyaml>=6.0.2",
"requests>=2.32.3", "requests>=2.32.3",
"simple-term-menu>=1.6.6",
"sqlalchemy>=2.0.40", "sqlalchemy>=2.0.40",
"sqlmodel>=0.0.24", "sqlmodel>=0.0.24",
"stomp.py", "stomp.py",
+11 -7
View File
@@ -88,7 +88,9 @@ if __name__ == "__main__":
) )
if len(server_list) > 1: if len(server_list) > 1:
for table, path in MAPPING.items(): for table, path in MAPPING.items():
mapping = create_item_id_mapping(logger, export_data[server_list[1].name][table]) mapping = create_item_id_mapping(
logger, export_data[server_list[1].name][table]
)
for item in export_data[server_list[0].name][table]: for item in export_data[server_list[0].name][table]:
logger.debug("checking %s:%s", table, item["id"]) logger.debug("checking %s:%s", table, item["id"])
check_item_id = item["id"] check_item_id = item["id"]
@@ -99,10 +101,11 @@ if __name__ == "__main__":
"checking values for %s != %s", item["id"], check_item["id"] "checking values for %s != %s", item["id"], check_item["id"]
) )
logger.debug("diff: %s\n%s", item, check_item) logger.debug("diff: %s\n%s", item, check_item)
result = server_list[1].update( if not args.dry_run:
logger, table, check_item_id, item result = server_list[1].update(
) logger, table, check_item_id, item
logger.info("update result: %s", result) )
logger.info("update result: %s", result)
else: else:
logger.debug( logger.debug(
"no changes for: %s(%s - %s)", "no changes for: %s(%s - %s)",
@@ -112,9 +115,10 @@ if __name__ == "__main__":
) )
else: else:
logger.info( logger.info(
"item %s in %s missing: ", check_item_id, server_list[1].name, item "item %s in %s missing: ", check_item_id, server_list[1].name
) )
server_list[1].create(logger, table, item) if not args.dry_run:
server_list[1].create(logger, table, item)
logger.info("synchronization of %s finished", table) logger.info("synchronization of %s finished", table)
logger.info("all tables synchronized") logger.info("all tables synchronized")
else: else:
+13
View File
@@ -362,6 +362,7 @@ version = "0.1.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "beautifulsoup4" }, { name = "beautifulsoup4" },
{ name = "click" },
{ name = "coverage" }, { name = "coverage" },
{ name = "fastapi", extra = ["standard"] }, { name = "fastapi", extra = ["standard"] },
{ name = "pathlib" }, { name = "pathlib" },
@@ -370,6 +371,7 @@ dependencies = [
{ name = "pytest-cov" }, { name = "pytest-cov" },
{ name = "pyyaml" }, { name = "pyyaml" },
{ name = "requests" }, { name = "requests" },
{ name = "simple-term-menu" },
{ name = "sqlalchemy" }, { name = "sqlalchemy" },
{ name = "sqlmodel" }, { name = "sqlmodel" },
{ name = "stomp-py" }, { name = "stomp-py" },
@@ -378,6 +380,7 @@ dependencies = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "beautifulsoup4", specifier = ">=4.13.4" }, { name = "beautifulsoup4", specifier = ">=4.13.4" },
{ name = "click", specifier = ">=8.1.8" },
{ name = "coverage", specifier = ">=7.8.0" }, { name = "coverage", specifier = ">=7.8.0" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" },
{ name = "pathlib", specifier = ">=1.0.1" }, { name = "pathlib", specifier = ">=1.0.1" },
@@ -386,6 +389,7 @@ requires-dist = [
{ name = "pytest-cov", specifier = ">=6.1.1" }, { name = "pytest-cov", specifier = ">=6.1.1" },
{ name = "pyyaml", specifier = ">=6.0.2" }, { name = "pyyaml", specifier = ">=6.0.2" },
{ name = "requests", specifier = ">=2.32.3" }, { name = "requests", specifier = ">=2.32.3" },
{ name = "simple-term-menu", specifier = ">=1.6.6" },
{ name = "sqlalchemy", specifier = ">=2.0.40" }, { name = "sqlalchemy", specifier = ">=2.0.40" },
{ name = "sqlmodel", specifier = ">=0.0.24" }, { name = "sqlmodel", specifier = ">=0.0.24" },
{ name = "stomp-py" }, { name = "stomp-py" },
@@ -744,6 +748,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
] ]
[[package]]
name = "simple-term-menu"
version = "1.6.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/80/f0f10b4045628645a841d3d98b584a8699005ee03a211fc7c45f6c6f0e99/simple_term_menu-1.6.6.tar.gz", hash = "sha256:9813d36f5749d62d200a5599b1ec88469c71378312adc084c00c00bfbb383893", size = 35493, upload_time = "2024-12-02T16:31:50.639Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/09/21d993e394c1fe5c44cd90453d88ed44932da8dfca006e424c072d77d29b/simple_term_menu-1.6.6-py3-none-any.whl", hash = "sha256:c2a869efa7a9f7e4a9c25858b42ca6974034951c137d5e281f5339b06ed8c9c2", size = 27600, upload_time = "2024-12-02T16:31:48.934Z" },
]
[[package]] [[package]]
name = "sniffio" name = "sniffio"
version = "1.3.1" version = "1.3.1"