[TikiWiki-commits] [Git][tikiwiki/tiki][master] [FIX] Enable batch downloading of folders in file galleries

Benoit Grégoire (@benoitg) via TikiWiki-cvs <[email protected]>
Newsgroups gmane.comp.cms.tiki.cvs
Message-ID <698396d46c401_3b18432c11885@gitlab-sidekiq-low-urgency-cpu-bound-v2-7d899c995b-qtqmx.mail>

Benoit Grégoire pushed to branch master at Tiki Wiki CMS Groupware / Tiki


Commits:
fd7de913 by Moïse Nturubika at 2026-02-04T18:49:32+00:00
[FIX] Enable batch downloading of folders in file galleries
---
* [FIX] Eliminate polymorphism in zip() and remove error suppression operator

* [REF] Improve zip method code quality per code review

* [FIX] Enable batch downloading of folders in file galleries

See merge request tikiwiki/tiki!9296

- - - - -


2 changed files:

- lib/Filegals/FileGalLib.php
- tiki-download_file.php


Changes:

=====================================
lib/Filegals/FileGalLib.php
=====================================
@@ -23,6 +23,7 @@ use Tiki\FileGallery\File as TikiFile;
 use Tiki\FileGallery\FileDraft as TikiFileDraft;
 use Tiki\FileGallery\ImageTransformer;
 use Tiki\Lib\Filegals\FileIsNotSafeException;
+use Symfony\Component\Filesystem\Filesystem;
 use TikiDb;
 use TikiLib;
 use TikiMail;
@@ -1266,10 +1267,22 @@ class FileGalLib extends TikiLib
         $this->table('tiki_files')->update(['maxhits' => (int) $limit], ['fileId' => (int) $fileId]);
     }
     // not the best optimisation as using a library using files and not content
-    public function zip($fileIds, &$error, $zipName = '')
+    /**
+     * Create a zip file from file gallery files
+     *
+     * @param array $fileEntries Array of file entries with structure:
+     *                           ['fileId' => int, 'path' => string]
+     *                           The 'path' key specifies the relative folder path within the zip
+     *                           to preserve folder hierarchy when downloading galleries.
+     * @param string &$error Error message on failure
+     * @param string $zipName Optional custom zip name
+     * @return array|false|null File info array on success, false on error, null on no permission
+     */
+    public function zip($fileEntries, &$error, $zipName = '')
     {
         global $tiki_p_admin_file_galleries, $prefs, $user;
         $userlib = TikiLib::lib('user');
+
         $list = [];
         $temp = '/' . md5(random_bytes(10)) . '/';
         if (! mkdir(sys_get_temp_dir() . $temp)) {
@@ -1280,24 +1293,48 @@ class FileGalLib extends TikiLib
             $error = "Can not create directory $temp";
             return false;
         }
-        $fileIds = array_unique($fileIds);
-        Perms::bulk(['type' => 'file'], 'object', $fileIds);
+        // Check permissions
+        $checkIds = array_unique(array_column($fileEntries, 'fileId'));
+        Perms::bulk(['type' => 'file'], 'object', $checkIds);
+
         $filenames = [];
-        $padding = strlen(count($fileIds)) - 1;
-        foreach ($fileIds as $fileId) {
+        $padding = strlen(count($fileEntries)) - 1;
+        foreach ($fileEntries as $fileEntry) {
+            $fileId = $fileEntry['fileId'];
+            $relativePath = $fileEntry['path'];
+
             $file = TikiFile::id($fileId);
             if ($tiki_p_admin_file_galleries == 'y' || $userlib->user_has_perm_on_object($user, $file->fileId, 'file', 'tiki_p_download_files')) {
                 if (empty($zipName)) {
                     $zipName = $file->galleryId;
                 }
+
+                $relativePath = str_replace('..', '', $relativePath);
+                $relativePath = trim($relativePath, '/\\');
+                $destDir = $temp;
+                if ($relativePath !== '') {
+                    $destDir .= $relativePath . '/';
+                    if (! is_dir($destDir)) {
+                        if (! mkdir($destDir, 0777, true)) {
+                            $error = "Cannot create directory: $destDir";
+                            return false;
+                        }
+                    }
+                }
+
                 $filename = $file->filename;
+                $pathKey = $relativePath;
+                if (! isset($filenames[$pathKey])) {
+                    $filenames[$pathKey] = [];
+                }
+
                 $counter = 1;
-                while (in_array($filename, $filenames)) {
+                while (in_array($filename, $filenames[$pathKey])) {
                     $filename = $file->filename . '_' . str_pad($counter, $padding, '0', STR_PAD_LEFT);
                     $counter++;
                 }
-                $filenames[] = $filename;
-                $tmp = $temp . $filename;
+                $filenames[$pathKey][] = $filename;
+                $tmp = $destDir . $filename;
                 if (! copy($file->getWrapper()->getReadableFile(), $tmp)) {
                     $error = "Can not copy to $tmp";
                     return false;
@@ -1312,7 +1349,7 @@ class FileGalLib extends TikiLib
         }
         $info['filename'] = "$zipName.zip";
         $zip = $temp . $info['filename'];
-        define('PCZLIB_SEPARATOR', '\001');
+        define('PCZLIB_SEPARATOR', '\\001');
         if (! $archive = new PclZip($zip)) {
             $error = $archive->errorInfo(true);
             return false;
@@ -1325,11 +1362,12 @@ class FileGalLib extends TikiLib
         $info['path'] = '';
         $info['filetype'] = 'application/x-zip-compressed';
         $info['filesize'] = strlen($info['data']);
-        foreach ($list as $tmp) {
-            unlink($tmp);
-        }
+
+        // Clean up temp directory using Symfony Filesystem
         unlink($zip);
-        rmdir($temp);
+        $fs = new Filesystem();
+        $fs->remove($temp);
+
         return $info;
     }
 


=====================================
tiki-download_file.php
=====================================
@@ -52,16 +52,64 @@ $zip = false;
 $error = '';
 
 if (! $skip) {
-    if (isset($_REQUEST['fileId'])) {
-        if (! is_array($_REQUEST['fileId'])) {
+    if (isset($_REQUEST['fileId']) || isset($_REQUEST['galId'])) {
+        $isBatch = (isset($_REQUEST['fileId']) && is_array($_REQUEST['fileId'])) || ! empty($_REQUEST['galId']);
+
+        if (! $isBatch) {
             if (isset($_GET['draft'])) {
                 $info = \Tiki\FileGallery\FileDraft::id($_REQUEST['fileId'])->getParams();
             } else {
                 $info = $filegallib->get_file($_REQUEST['fileId']);
             }
         } else {
+            // Collect file entries in structured format: ['fileId' => int, 'path' => string]
+            $fileEntries = [];
+
+            // Convert plain fileIds from request to structured format
+            $requestFileIds = $_REQUEST['fileId'] ?? [];
+            if (! is_array($requestFileIds)) {
+                $requestFileIds = [$requestFileIds];
+            }
+            foreach ($requestFileIds as $fid) {
+                $fileEntries[] = ['fileId' => $fid, 'path' => ''];
+            }
+
+            // Process galleries recursively to collect files with paths
+            if (! empty($_REQUEST['galId'])) {
+                $galIds = is_array($_REQUEST['galId']) ? $_REQUEST['galId'] : [$_REQUEST['galId']];
+
+                $visited = [];
+                $processGallery = function ($galId, $currentPath) use (&$fileEntries, $filegallib, &$processGallery, &$visited) {
+                    if (in_array($galId, $visited)) {
+                        return;
+                    }
+                    $visited[] = $galId;
+
+                    $res = $filegallib->get_files(0, -1, 'name_asc', '', $galId, false, true, false, true, false, false, false, false);
+                    if (! empty($res['data'])) {
+                        foreach ($res['data'] as $item) {
+                            // Galleries have both fileId and galleryId; use 'id' for the actual gallery ID
+                            if (! empty($item['isgal']) || (isset($item['galleryId']) && $item['id'] == $item['galleryId'])) {
+                                $subGalId = $item['id'];
+                                if ($subGalId != $galId && ! in_array($subGalId, $visited)) {
+                                    $processGallery($subGalId, $currentPath . $item['name'] . '/');
+                                }
+                            } elseif (isset($item['fileId'])) {
+                                $fileEntries[] = ['fileId' => $item['fileId'], 'path' => $currentPath];
+                            }
+                        }
+                    }
+                };
+
+                foreach ($galIds as $gId) {
+                    $gInfo = $filegallib->get_file_gallery_info($gId);
+                    if ($gInfo) {
+                        $processGallery($gId, $gInfo['name'] . '/');
+                    }
+                }
+            }
             $zipName = $_REQUEST['zipName'] ?? '';
-            $info = $filegallib->zip($_REQUEST['fileId'], $error, $zipName);
+            $info = $filegallib->zip($fileEntries, $error, $zipName);
             $zip = true;
         }
     } elseif (isset($_REQUEST['galleryId']) && isset($_REQUEST['name'])) {
@@ -94,7 +142,7 @@ if (! $skip) {
             TikiLib::lib('cache')->cacheItem($src, serialize($info), 'external_downloaded_files');
         }
     } else {
-        $access->display_error('', tra('Incorrect param'), 400);
+        $access->display_error('', tra('Incorrect param. No file or gallery selected for download.'), 400);
     }
     if (! is_array($info)) {
         $access->display_error(null, tra('File has been deleted'), 404);



View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/fd7de9131ffd0619f747322efb5d8059437511d1

-- 
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/fd7de9131ffd0619f747322efb5d8059437511d1
You're receiving this email because of your account on gitlab.com.

_______________________________________________
TikiWiki-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.