Browse Source

feat: add configurable compression level for zip downloads (Folder Share page, downloadPageFolder.html) (#179)

* feat: Add image thumbnail preview on row hover (downloadPageFolder)

- Display thumbnail preview when hovering over image rows
- Preview follows cursor movement with offset
- Apply hover effect on entire row for better UX

* feat: add configurable compression level for zip downloads (Folder Share page, downloadPageFolder.html)

Added the ability to customize zip file compression levels:

- Added new ArozZipFileWithCompressionLevel function that accepts compression level parameter
- Added compression_level query parameter support to share downloads
- Added UI checkbox to toggle between compressed/uncompressed downloads
- Auto-detect when folder contains mostly media files (>= 50% in size) and pre-check uncompressed option
- Updated compression help text to provide guidance on media files
- Default compression level remains at flate.DefaultCompression (-1)

The changes allow users to choose no compression for media-heavy folders to improve zipping speed, while maintaining regular compression for other content types.

* lazy bug fix
Saren Arterius 1 tháng trước cách đây
mục cha
commit
cb3a1960eb

+ 8 - 0
src/mod/filesystem/fileOpr.go

@@ -332,6 +332,11 @@ To use it with local file system, pass in nil in fsh for each item in filelist,
 filesystem.ArozZipFile([]*filesystem.FileSystemHandler{nil}, []string{zippingSource}, nil, targetZipFilename, false)
 */
 func ArozZipFile(sourceFshs []*FileSystemHandler, filelist []string, outputFsh *FileSystemHandler, outputfile string, includeTopLevelFolder bool) error {
+	// Call the new function with default compression level (e.g., 6)
+	return ArozZipFileWithCompressionLevel(sourceFshs, filelist, outputFsh, outputfile, includeTopLevelFolder, flate.DefaultCompression)
+}
+
+func ArozZipFileWithCompressionLevel(sourceFshs []*FileSystemHandler, filelist []string, outputFsh *FileSystemHandler, outputfile string, includeTopLevelFolder bool, compressionLevel int) error {
 	//Create the target zip file
 	var file arozfs.File
 	var err error
@@ -347,6 +352,9 @@ func ArozZipFile(sourceFshs []*FileSystemHandler, filelist []string, outputFsh *
 	defer file.Close()
 
 	writer := zip.NewWriter(file)
+	writer.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) {
+		return flate.NewWriter(out, compressionLevel)
+	})
 	defer writer.Close()
 
 	for i, srcpath := range filelist {

+ 13 - 1
src/mod/share/share.go

@@ -8,6 +8,7 @@ package share
 */
 
 import (
+	"compress/flate"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -267,6 +268,17 @@ func (s *Manager) HandleShareAccess(w http.ResponseWriter, r *http.Request) {
 	directServe := false
 	relpath := ""
 
+	compressionLevel := flate.DefaultCompression
+	if compressionStr := r.URL.Query().Get("compression_level"); compressionStr != "" {
+		if val, err := strconv.Atoi(compressionStr); err == nil {
+			// Validate compression level range (-2 to 9)
+			if val >= -2 && val <= 9 {
+				compressionLevel = val
+			}
+			// Optional: else could return an error or just silently use default value
+		}
+	}
+
 	id, err := utils.GetPara(r, "id")
 	if err != nil {
 		//ID is not defined in the URL paramter. New ID defination is based on the subpath content
@@ -615,7 +627,7 @@ func (s *Manager) HandleShareAccess(w http.ResponseWriter, r *http.Request) {
 					}
 
 					//Build a filelist
-					err := filesystem.ArozZipFile([]*filesystem.FileSystemHandler{zippingSourceFsh}, []string{zippingSource}, nil, targetZipFilename, false)
+					err := filesystem.ArozZipFileWithCompressionLevel([]*filesystem.FileSystemHandler{zippingSourceFsh}, []string{zippingSource}, nil, targetZipFilename, false, compressionLevel)
 					if err != nil {
 						//Failed to create zip file
 						w.WriteHeader(http.StatusInternalServerError)

+ 74 - 10
src/system/share/downloadPageFolder.html

@@ -114,12 +114,28 @@
                           </tr>
                         </tbody>
                       </table>
-                    <a href="{{downloadurl}}"><button class="button-primary">Download All</button></a>
-                    <button id="sharebtn" onclick="share();">Share</button>
-                    <p style="font-size: 80%;"><b>Depending on folder size, zipping might take a while to complete.</b></p>
-                    <p>Request File ID: {{reqid}}<br>
-                    Request Timestamp: {{reqtime}}</p>
-                    <small>📂 Double click any item in the list to open or download</small>
+                      <a href="{{downloadurl}}" id="downloadLink"><button class="button-primary">Download All</button></a>
+                      <button id="sharebtn" onclick="share();">Share</button>
+                      <div style="margin-top: 10px;">
+                          <input type="checkbox" id="uncompressedCheck" name="uncompressed">
+                          <label for="uncompressedCheck" style="display: inline">Uncompressed</label>
+                      </div>
+                      <p style="font-size: 80%;"><b>Zipping duration depends on folder size and compression settings. Media files are best zipped without compression for faster processing.</b></p>
+                      <p>Request File ID: {{reqid}}<br>
+                      Request Timestamp: {{reqtime}}</p>
+                      <small>📂 Double click any item in the list to open or download</small>
+                      
+                      <script>
+                      document.getElementById('uncompressedCheck').addEventListener('change', function() {
+                          const downloadLink = document.getElementById('downloadLink');
+                          if (this.checked) {
+                              downloadLink.href = '{{downloadurl}}?compression_level=0';
+                          } else {
+                              downloadLink.href = '{{downloadurl}}';
+                          }
+                      });
+                      </script>
+                      
                     
                 </div>
                 <div class="one-half column" id="filelistWrapper" style="overflow-y: auto; padding-right: 0.5em; min-height: 400px;">
@@ -149,7 +165,14 @@
       var downloadUUID = `{{downloaduuid}}`;
       var currentViewingRoot = ".";
       var selectedFile = null;
-      renderFileList(treeFileList["."]);
+      var stats = renderFileList(treeFileList["."]);
+
+      // most files are already compressed media...
+      if (stats.totalCompressedMediaSize / stats.totalFileSize >= 0.5) {
+        document.getElementById('uncompressedCheck').checked = true;
+        const downloadLink = document.getElementById('downloadLink');
+        downloadLink.href = '{{downloadurl}}?compression_level=0';
+      }
 
       handleWindowResize();
       $(window).on("resize", function(e){
@@ -188,6 +211,32 @@
         }
       }
       
+      function convertToBytes(sizeString) {
+        // Remove any spaces and convert to uppercase
+        sizeString = sizeString.replace(/\s+/g, '').toUpperCase();
+        
+        // Regular expression to match number and unit
+        const matches = sizeString.match(/^([\d.]+)([KMGT]?B)$/i);
+        
+        if (!matches) {
+            throw new Error('Invalid format');
+        }
+
+        const size = parseFloat(matches[1]);
+        const unit = matches[2];
+
+        // Conversion factors
+        const units = {
+            'B': 1,
+            'KB': 1024,
+            'MB': 1024 ** 2,
+            'GB': 1024 ** 3,
+            'TB': 1024 ** 4
+        };
+
+        return Math.round(size * units[unit]);
+      }
+
 
       function renderFileList(filelist){
         $("#folderList").html("");
@@ -216,26 +265,37 @@
           `);
         }
 
+        var totalCompressedMediaSize = 0;
+        var totalFileSize = 0;
         filelist.forEach(file => {
           var filetype = "File";
           var displayName = "";
           var isImage = false;
+          
           if (file.IsDir == true){
             //Folder
             filetype = "Folder";
             displayName = "📁 " + file.Filename;
           }else{
             //File
+            totalFileSize += convertToBytes(file.Filesize);
             var ext = file.Filename.split(".").pop();
             var icon = "📄"
             ext = ext.toLowerCase();
-            if (ext == "mp3" || ext == "wav" || ext == "flac" || ext == "aac" || ext == "ogg" || ext == ""){
+            if (ext == "mp3" || ext == "wav" || ext == "flac" || ext == "alac" || ext == "wma" || ext == "aac" || ext == "ogg" || ext == ""){
               icon = "🎵";
-            }else if (ext == "mp4" || ext == "avi" || ext == "webm" || ext == "mkv" || ext == "mov" || ext == "rvmb"){
+              if (ext != "wav") {
+                totalCompressedMediaSize += convertToBytes(file.Filesize);
+              }
+            }else if (ext == "mp4" || ext == "avi" || ext == "webm" || ext == "mkv" || ext == "wmv" || ext == "mov" || ext == "rmvb" || ext == "rm"){
               icon = "🎞️";
-            }else if (ext == "png" || ext == "jpeg" || ext == "jpg" || ext == "bmp" || ext == "gif"){
+              totalCompressedMediaSize += convertToBytes(file.Filesize);
+            }else if (ext == "png" || ext == "jpeg" || ext == "jpg" || ext == "bmp" || ext == "gif" || ext == "webp" || ext == "avif"){
               icon = "🖼️";
               isImage = true;
+              if (ext != "bmp") {
+                totalCompressedMediaSize += convertToBytes(file.Filesize);
+              }
             }
 
             displayName =  icon + " " + file.Filename;
@@ -293,6 +353,10 @@
             .fileobject:hover { background-color: rgba(0,0,0,0.05); }
           `)
           .appendTo("head");
+
+        return {
+          totalCompressedMediaSize, totalFileSize
+        }
       }
 
       //Went up one level