Initial commit
This commit is contained in:
+131
@@ -0,0 +1,131 @@
|
||||
"""Redump 7z store: scan archives and extract the disc image inside.
|
||||
|
||||
Store archives are ``Name (Region) (tags).7z`` with the ``.iso`` (or ``.cso``)
|
||||
inside. Extraction shells out to the system ``7z`` binary.
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
log = logging.getLogger("store")
|
||||
|
||||
_ARCHIVE_EXTS = {".7z", ".zip"}
|
||||
_IMAGE_EXTS = {".iso", ".cso", ".img", ".bin"}
|
||||
|
||||
|
||||
class TransferCancelled(Exception):
|
||||
"""Raised inside a transfer when the user cancels it."""
|
||||
|
||||
|
||||
def is_archive(path):
|
||||
return Path(path).suffix.lower() in _ARCHIVE_EXTS
|
||||
|
||||
|
||||
def display_name(name):
|
||||
"""Display title of a store archive (Redump names are kept as-is)."""
|
||||
return str(name).strip()
|
||||
|
||||
|
||||
def region(name):
|
||||
"""Extract the region tag (first trailing parenthetical) from a store name."""
|
||||
s = str(name)
|
||||
tags = []
|
||||
while True:
|
||||
m = re.search(r"\s*\(([^()]*)\)\s*$", s)
|
||||
if not m:
|
||||
break
|
||||
tags.insert(0, m.group(1).strip())
|
||||
s = s[: m.start()]
|
||||
return tags[0] if tags else ""
|
||||
|
||||
|
||||
def scan(root, limit=None):
|
||||
"""Return ``[{path, name, size, mtime_ns}]`` for archives in the store."""
|
||||
root = Path(root)
|
||||
if not root.is_dir():
|
||||
return []
|
||||
items = []
|
||||
for path in sorted(root.iterdir()):
|
||||
if not path.is_file() or not is_archive(path):
|
||||
continue
|
||||
try:
|
||||
stat = path.stat()
|
||||
except OSError:
|
||||
continue
|
||||
items.append({
|
||||
"path": str(path),
|
||||
"name": path.stem,
|
||||
"size": stat.st_size,
|
||||
"mtime_ns": stat.st_mtime_ns,
|
||||
})
|
||||
if limit is not None and len(items) >= limit:
|
||||
break
|
||||
return items
|
||||
|
||||
|
||||
def peek_archive(path, cancel_event=None):
|
||||
"""Return the disc-image filename inside a 7z archive, or None."""
|
||||
path = Path(path)
|
||||
if not path.is_file():
|
||||
return None
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
["7z", "l", "-slt", str(path)],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1,
|
||||
)
|
||||
except OSError:
|
||||
return None
|
||||
name = None
|
||||
for line in proc.stdout:
|
||||
if cancel_event is not None and cancel_event.is_set():
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
raise TransferCancelled()
|
||||
if line.startswith("Path = "):
|
||||
candidate = line[7:].strip()
|
||||
if candidate.lower().endswith(tuple(_IMAGE_EXTS)):
|
||||
name = candidate
|
||||
code = proc.wait()
|
||||
if code != 0:
|
||||
return None
|
||||
return name
|
||||
|
||||
|
||||
def extract_iso(archive, iso_name, out_dir, progress_cb=None, cancel_event=None):
|
||||
"""Extract one disc image from a 7z archive; return its path."""
|
||||
archive = Path(archive)
|
||||
out_dir = Path(out_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
cmd = ["7z", "x", "-y", f"-o{out_dir}", str(archive), iso_name]
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1,
|
||||
)
|
||||
except OSError as exc:
|
||||
raise RuntimeError(f"7z is not available: {exc}") from exc
|
||||
|
||||
last_pct = 0.0
|
||||
for line in proc.stdout:
|
||||
if cancel_event is not None and cancel_event.is_set():
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
raise TransferCancelled()
|
||||
m = re.search(r"(\d{1,3})\s*%", line)
|
||||
if m:
|
||||
last_pct = int(m.group(1)) / 100.0
|
||||
if progress_cb is not None:
|
||||
progress_cb("Extracting", last_pct, line.strip()[:80])
|
||||
code = proc.wait()
|
||||
if code != 0:
|
||||
raise RuntimeError(f"7z extraction failed (exit {code})")
|
||||
result = out_dir / iso_name
|
||||
if not result.exists():
|
||||
raise RuntimeError("7z extraction completed but the image was not found")
|
||||
return result
|
||||
Reference in New Issue
Block a user