Compare commits

..

7 Commits

Author SHA1 Message Date
Terranom674
7b915c310a 0.9.6.29: PHP-Abfrage stabilisieren 2026-08-19 19:58:28 +02:00
Terranom674
6df469ab7a 0.9.6.29: Shadowtree stabilisieren und Namensprüfung entfernen 2026-08-19 19:55:32 +02:00
Terranom674
55294643b4 0.9.6.29: Shadowtree- und Sync-Umbau zurückrollen 2026-08-19 19:54:15 +02:00
Terranom674
0f3c78f02c Merge pull request #18 from Terranom674/fix/09628-single-piwigo-structure
0.9.6.28: nur ein Piwigo-Strukturpfad
2026-08-19 19:44:36 +02:00
Terranom674
d80163a704 0.9.6.28: Version fuer Strukturfix anheben 2026-08-19 19:43:48 +02:00
Terranom674
9e57c4ea3d 0.9.6.28: Fallback nutzt denselben Piwigo-Syncpfad 2026-08-19 19:43:30 +02:00
Terranom674
30f1c0af18 Merge pull request #17 from Terranom674/fix/09627-webdav-album-structure
0.9.6.27: WebDAV ohne Parallel-Albumstruktur
2026-08-19 19:39:07 +02:00
5 changed files with 47 additions and 56 deletions

View File

@@ -62,18 +62,12 @@ SELECT id, dir, name
OR LOWER(name) = LOWER(\''.$name_sql.'\') OR LOWER(name) = LOWER(\''.$name_sql.'\')
) )
ORDER BY CASE WHEN dir = \''.$dir_sql.'\' THEN 0 ELSE 1 END, id ORDER BY CASE WHEN dir = \''.$dir_sql.'\' THEN 0 ELSE 1 END, id
LIMIT 1
;'; ;';
$result = pwg_query($query); $result = pwg_query($query);
$ids = array(); if (!pwg_db_num_rows($result)) return null;
while ($row = pwg_db_fetch_assoc($result)) $row = pwg_db_fetch_assoc($result);
{ return (int)$row['id'];
$ids[] = (int)$row['id'];
}
if (count($ids) > 1)
{
throw new RuntimeException('Albumzuordnung ist mehrdeutig fuer "'.$name.'".');
}
return count($ids) === 1 ? $ids[0] : null;
} }
function bratonien_tools_nc_ensure_album_path($relative_dir, $excluded_site_id, array &$cache, array &$created_ids) function bratonien_tools_nc_ensure_album_path($relative_dir, $excluded_site_id, array &$cache, array &$created_ids)
@@ -175,10 +169,6 @@ function bratonien_tools_ws_nc_sync_productive($params, &$service)
try try
{ {
// Older WebDAV builds created a complete physical album tree for their own
// Piwigo site. Remove that technical duplicate first. The WebDAV files are
// placeholders and are rebuilt independently, therefore database deletion
// must not remove the actual Nextcloud originals.
$counts['removed_duplicate_categories'] = bratonien_tools_nc_remove_storage_categories($site_id); $counts['removed_duplicate_categories'] = bratonien_tools_nc_remove_storage_categories($site_id);
list($dbnow) = pwg_db_fetch_row(pwg_query('SELECT NOW()')); list($dbnow) = pwg_db_fetch_row(pwg_query('SELECT NOW()'));
@@ -286,7 +276,6 @@ function bratonien_tools_ws_nc_sync_productive($params, &$service)
$counts['new_elements'] = count($new_ids); $counts['new_elements'] = count($new_ids);
} }
// Refresh the normal file attributes for connector-managed images only.
$updates = array(); $updates = array();
foreach ($all_ids as $id) foreach ($all_ids as $id)
{ {

View File

@@ -1,7 +1,7 @@
<?php <?php
/* /*
Plugin Name: Bratonien Tools Plugin Name: Bratonien Tools
Version: 0.9.6.27 Version: 0.9.6.29
Description: Erweiterbare Administrationswerkzeuge fuer die Bratonien-Piwigo-Installation. Description: Erweiterbare Administrationswerkzeuge fuer die Bratonien-Piwigo-Installation.
Plugin URI: https://github.com/Terranom674/Piwigo_Bratonien_Tools Plugin URI: https://github.com/Terranom674/Piwigo_Bratonien_Tools
Author: Bratonien Author: Bratonien

22
runtime/lib/build_webdav_placeholder_source.py Normal file → Executable file
View File

@@ -3,7 +3,7 @@
This creates only tiny placeholder files plus a metadata mapping; no Nextcloud This creates only tiny placeholder files plus a metadata mapping; no Nextcloud
original media is downloaded. The authenticated Nextcloud user is never used as original media is downloaded. The authenticated Nextcloud user is never used as
an album name. Selecting the user's WebDAV root mirrors its children directly. an album name.
""" """
from __future__ import annotations from __future__ import annotations
@@ -249,20 +249,32 @@ def main() -> int:
for remote_root_raw in args.root: for remote_root_raw in args.root:
remote_root = validate_relative(remote_root_raw) remote_root = validate_relative(remote_root_raw)
current, _ = client.list_collection(remote_root) current, root_children = client.list_collection(remote_root)
fileid = int(current["fileid"]) fileid = int(current["fileid"])
if fileid in used_fileids: if fileid in used_fileids:
fail(f"duplicate selected Nextcloud root fileid: {fileid}") fail(f"duplicate selected Nextcloud root fileid: {fileid}")
used_fileids.add(fileid) used_fileids.add(fileid)
# An explicitly selected folder keeps its own name. The authenticated
# user's WebDAV root is transparent and must never become an album.
display = "" if remote_root == "" else (str(current.get("display_name", "")).strip() or PurePosixPath(remote_root).name)
local_name = f"root-{fileid}" local_name = f"root-{fileid}"
local_root = staging / local_name local_root = staging / local_name
files, folders, skipped = build_root(client, remote_root, local_root, seed, mapping) files, folders, skipped = build_root(client, remote_root, local_root, seed, mapping)
total_files += files total_files += files
total_folders += folders total_folders += folders
total_skipped += skipped total_skipped += skipped
if remote_root == "":
for child in sorted(root_children, key=lambda item: str(item.get("display_name", "")).casefold()):
name = safe_local_name(str(child.get("display_name", "")))
child_fileid = int(child.get("fileid", 0))
if child_fileid < 1:
fail(f"Nextcloud returned no stable fileid for root child {name!r}")
child_path = source_dir / local_name / name
if bool(child.get("is_dir")):
manifest.append(f"webdav:{child_fileid}\tfolder\t{name}\t{child_path}")
elif Path(name).suffix.lower() in SUPPORTED_IMAGE_EXTENSIONS:
manifest.append(f"webdav:{child_fileid}\tfile\t{name}\t{child_path}")
continue
display = str(current.get("display_name", "")).strip() or PurePosixPath(remote_root).name
manifest.append(f"webdav:{fileid}\tfolder\t{display}\t{source_dir / local_name}") manifest.append(f"webdav:{fileid}\tfolder\t{display}\t{source_dir / local_name}")
if previous.exists(): if previous.exists():

29
runtime/lib/piwigo-sync.php Normal file → Executable file
View File

@@ -252,8 +252,6 @@ try
); );
decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncProductive', 'site_id'=>$site_id), $headers)); decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncProductive', 'site_id'=>$site_id), $headers));
$orphan = decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>$site_id, 'simulate'=>0), $headers)); $orphan = decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>$site_id, 'simulate'=>0), $headers));
// Entfernt alte technische bratonien-webdav-N Wrapper aus Site 1,
// nachdem deren generierte Verzeichnisse beim Reconcile entfernt wurden.
if ($site_id !== 1) if ($site_id !== 1)
{ {
decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0), $headers)); decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0), $headers));
@@ -284,16 +282,31 @@ try
try try
{ {
decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'pwg.session.login', 'username'=>$fallback_user, 'password'=>$fallback_password), array(), $cookie_file)); decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'pwg.session.login', 'username'=>$fallback_user, 'password'=>$fallback_password), array(), $cookie_file));
http_request(
$base_url.'/admin.php?page=site_update&site='.$site_id, // Der Fallback darf keinen zweiten Strukturpfad benutzen. Auch mit
array('sync'=>'files','display_info'=>1,'privacy_level'=>0,'sync_meta'=>1,'simulate'=>0,'subcats-included'=>1,'bratonien_connector'=>1,'submit'=>1), // Benutzer/Passwort wird exakt derselbe Bratonien-Sync wie mit API-Key
// ausgefuehrt. Dadurch werden alte technische WebDAV-Kategorien entfernt
// und vorhandene Piwigo-Alben wiederverwendet.
decode_ws(http_request(
$base_url.'/ws.php?format=json',
array('method'=>'bratonien.nc.syncProductive', 'site_id'=>$site_id),
array(), array(),
$cookie_file $cookie_file
); ));
$orphan = decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>$site_id, 'simulate'=>0), array(), $cookie_file)); $orphan = decode_ws(http_request(
$base_url.'/ws.php?format=json',
array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>$site_id, 'simulate'=>0),
array(),
$cookie_file
));
if ($site_id !== 1) if ($site_id !== 1)
{ {
decode_ws(http_request($base_url.'/ws.php?format=json', array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0), array(), $cookie_file)); decode_ws(http_request(
$base_url.'/ws.php?format=json',
array('method'=>'bratonien.nc.syncOrphans', 'site_id'=>1, 'simulate'=>0),
array(),
$cookie_file
));
} }
$added = (int)($orphan['added_orphans'] ?? 0); $added = (int)($orphan['added_orphans'] ?? 0);
$deleted = (int)($orphan['deleted_orphans'] ?? 0); $deleted = (int)($orphan['deleted_orphans'] ?? 0);

31
runtime/lib/shadow_tree.py Normal file → Executable file
View File

@@ -79,23 +79,15 @@ def preferred_target(source_key: str, raw_name: str, parent_target: Path, old_ma
return safe_name(raw_name) return safe_name(raw_name)
def mirror_directory( def mirror_directory(source: Path, target: Path, source_key: str, target_key: Path, old_map: dict[str, str], new_map: dict[str, str]) -> None:
source: Path,
target: Path,
source_key: str,
target_key: Path,
old_map: dict[str, str],
new_map: dict[str, str],
used: set[str] | None = None,
) -> None:
target.mkdir(parents=True, exist_ok=True) target.mkdir(parents=True, exist_ok=True)
used_names = used if used is not None else set() used: set[str] = set()
for child in sorted(source.iterdir(), key=lambda item: (item.name.casefold(), item.name)): for child in sorted(source.iterdir(), key=lambda item: (item.name.casefold(), item.name)):
if child.is_symlink(): if child.is_symlink():
continue continue
child_source_key = f"{source_key}/{child.name}" child_source_key = f"{source_key}/{child.name}"
preferred = preferred_target(child_source_key, child.name, target_key, old_map) preferred = preferred_target(child_source_key, child.name, target_key, old_map)
child_name = unique_name(preferred, used_names, child.is_file()) child_name = unique_name(preferred, used, child.is_file())
child_target = target / child_name child_target = target / child_name
child_target_key = target_key / child_name child_target_key = target_key / child_name
new_map[child_source_key] = child_target_key.as_posix() new_map[child_source_key] = child_target_key.as_posix()
@@ -128,26 +120,11 @@ def build(manifest: Path, destination: Path, state_file: Path) -> None:
try: try:
used_roots: set[str] = set() used_roots: set[str] = set()
transparent_roots = 0
for entry in sorted(entries, key=lambda item: (item["display_name"].casefold(), item["share_id"])): for entry in sorted(entries, key=lambda item: (item["display_name"].casefold(), item["share_id"])):
source = Path(entry["source_path"]) source = Path(entry["source_path"])
source_key = f"share:{entry['share_id']}" source_key = f"share:{entry['share_id']}"
is_file_share = entry["item_type"] == "file"
# Empty display_name is intentional: it represents the authenticated
# user's WebDAV root. Its children belong directly at destination;
# the Nextcloud username must never become an album wrapper.
if not is_file_share and entry["display_name"] == "":
transparent_roots += 1
if transparent_roots > 1:
raise ValueError("only one transparent WebDAV root is allowed")
if not source.is_dir():
raise FileNotFoundError(f"source is not a readable directory: {source}")
new_map[source_key] = "."
mirror_directory(source, staging, source_key, Path("."), old_map, new_map, used_roots)
continue
preferred = preferred_target(source_key, entry["display_name"], Path("."), old_map) preferred = preferred_target(source_key, entry["display_name"], Path("."), old_map)
is_file_share = entry["item_type"] == "file"
root_name = unique_name(preferred, used_roots, is_file_share) root_name = unique_name(preferred, used_roots, is_file_share)
root_key = Path(root_name) root_key = Path(root_name)
new_map[source_key] = root_key.as_posix() new_map[source_key] = root_key.as_posix()