From 2821d29a87b28785d74fa64d975465e4c99d4416 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Ledure Date: Tue, 6 Oct 2026 14:52:50 +0200 Subject: [PATCH] ScriptForge (FileSystem) Manage ZIP archives The FileSystem service provides simple methods to manage compressed zip archives. As an example, .odf and .oxt files being formatted as such archives, massive or punctual changes of file contents with smart scripts can be facilitated. New methods: - IsZipFile(filename) returns True when filename is a valid zip file - FilesInZip(filename, pattern) returns a list of archive members, optionally filtered with a Like pattern, stored in filename - ExtractFromZip(filename, destination, source) stores the source file(s) found in the given ZIP file into a destination folder - CompressToZip(filename, foldername, rootfolder) stores the content of folder, including its subfolders, into a zip archive, optionally under a root folder The implementation is based on the python zipfile standard library. The methods area available both for Basic and Python user scripts. For Python scripts, the user is also free to use the native zipfile library instead. The user documentation shouid be updated according to this commit. Change-Id: I506a8583dc3fa38c7e05094562ade33f84a24747 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/211891 Tested-by: Jenkins Reviewed-by: Jean-Pierre Ledure --- wizards/source/scriptforge/SF_FileSystem.xba | 301 ++++++++++++++++++ .../scriptforge/python/ScriptForgeHelper.py | 158 ++++++--- .../source/scriptforge/python/scriptforge.py | 17 + .../source/scriptforge/python/scriptforge.pyi | 66 ++++ 4 files changed, 499 insertions(+), 43 deletions(-) diff --git a/wizards/source/scriptforge/SF_FileSystem.xba b/wizards/source/scriptforge/SF_FileSystem.xba index fd166ed524dc..1d7eda971a25 100644 --- a/wizards/source/scriptforge/SF_FileSystem.xba +++ b/wizards/source/scriptforge/SF_FileSystem.xba @@ -54,6 +54,14 @@ Option Explicit ''' The file manifest.xml is managed automatically. ''' The FileNaming setting is ignored. ''' +''' The FileSystem service provides also simple methods to extract files from or load files +''' into archive (zip) files. +''' .odf and .oxt files being compressed files, direct vreation or update of such files (with care !) is made easy. +''' Note that multipart and encrypted zip files are not supported. +''' +''' The service provides symbolic names for a number of system folders (home, LibreOffice install, ...) +''' that contribute to improving the portability of user scripts. +''' ''' Service invocation example: ''' Dim FSO As Variant ''' Set FSO = CreateScriptService("FileSystem") @@ -377,6 +385,96 @@ CatchNotSupported: GoTo Finally End Function ' ScriptForge.SF_FileSystem.CompareFiles +REM ----------------------------------------------------------------------------- +Public Function CompressToZip(Optional ByVal FileName As Variant _ + , Optional ByVal FolderName As Variant _ + , Optional ByVal RootFolder As Variant _ + ) As Variant +''' Stores the content of Folder, including its subfolders, into a zip archive. +''' Encrypted archives are not supported. +''' The compressed files are stored optionally under a RootFolder. +''' Files existing already in the archive are not overwritten. +''' The method is not supported for document's internal file systems. +''' Args: +''' FileName: A string representing the zip file. +''' If it does not exist, the archive is created, oncluding its parent folders. +''' All files present in the archive are kept untouched. +''' FolderName: The folder to compress, as a string. +''' All files and subfolders are included. The Folder itself is not. +''' RootFolder: When absent, files and folders are loaded in the archive from the root ("/"). +''' When a RootFolder is given, as a string without path separator, they are stored below that folder. +''' Returns: +''' True when successful. +''' When False, an error message might be found in the console. +''' Exceptions: +''' UNKNOWNFOLDERERROR Source folder does not exist +''' NOTAFOLDERERROR The folder is a file, not a folder +''' FILESYSTEMERROR The method is not applicable on document's file systems +''' Example: +''' a = SF_FileSystem.CompressToZip("C:\archive.zip", Folder := "C:\Downloads") + +Dim bCompress As Boolean ' Return value +Dim vCompress As Variant ' Returned from Python (True/False, error message) +Dim sFile As String ' Alias of FileName +Dim sMode As String ' a = append, file exists - x = exclusive read/write +Dim sParentFolder As String ' Parent folder of FileName +Dim vRootFolder As Variant ' Alias of RootFolder +Const cstPyHelper = "$" & "_SF_FileSystem__CompressToZip" +Const cstThisSub = "FileSystem.CompressToZip" +Const cstSubArgs = "FileName, FolderName, [RootFolder=""""]" + + If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch + bCompress = False + +Check: + If IsMissing(RootFolder) Or IsEmpty(RootFolder) Then RootFolder = "" + If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then + If Not SF_Utils._ValidateFile(FileName, "FileName") Then GoTo Finally + If Not SF_Utils._ValidateFile(FolderName, "FolderName") Then GoTo Finally + If Not SF_Utils._Validate(RootFolder, "RootFolder", V_STRING) Then GoTo Finally + End If + If SF_FileSystem._IsDocFileSystem(FileName) Then GoTo CatchNotSupported + + If SF_FileSystem.FileExists(FolderName) Then GoTo CatchFileNotFolder + If Not SF_FileSystem.FolderExists(FolderName) Then GoTo CatchFolderNotExists + +Try: + With ScriptForge.SF_FileSystem + ' Append (a) or exclusive create (x) mode ? Eventually, create parent folder + If .FileExists(FileName) Then + sMode = "a" + Else + sParentFolder = .GetParentFolderName(FileName) + If Not .FolderExists(sParentFolder) Then .CreateFolder(sParentFolder) + sMode = "x" + End If + If Len(RootFolder) > 0 Then vRootFolder = RootFolder Else vRootFolder = Null + With ScriptForge.SF_Session + ' Python requires OS notation for files and folders + vCompress = .ExecutePythonScript(.SCRIPTISSHARED, _SF_.PythonHelper & cstPyHelper _ + , _ConvertFromUrl(FileName), sMode, ConvertFromUrl(FolderName), vRootFolder) + End With + bCompress = vCompress(0) + If Not bCompress Then SF_Exception.DebugPrint("CompressToZip", vCompress(1)) + End With + +Finally: + CompressToZip = bCompress + SF_Utils._ExitFunction(cstThisSub) + Exit Function +Catch: + GoTo Finally +CatchFolderNotExists: + SF_Exception.RaiseFatal(UNKNOWNFOLDERERROR, "FolderName", FolderName) + GoTo Finally +CatchFileNotFolder: + SF_Exception.RaiseFatal(NOTAFOLDERERROR, "FolderName", FolderName) + GoTo Finally +CatchNotSupported: + SF_Exception.RaiseFatal(FILESYSTEMERROR, "FileName", Split(cstThisSub, ".")(1), FileName) + GoTo Finally +End Function ' ScriptForge.SF_FileSystem.CompressToZip + REM ----------------------------------------------------------------------------- Public Function CopyFile(Optional ByVal Source As Variant _ , Optional ByVal Destination As Variant _ @@ -725,6 +823,85 @@ Catch: GoTo Finally End Function ' ScriptForge.SF_FileSystem.ExtensionFolder +REM ----------------------------------------------------------------------------- +Public Function ExtractFromZip(Optional ByVal FileName As Variant _ + , Optional ByVal Destination As Variant _ + , Optional ByVal Source As Variant _ + ) As Variant +''' Extracts a list of files contained in a zip container and stores them in a folder. +''' The process ends at the first error. Existing files are overwritten without warning. +''' The method is not supported for document's internal file systems. +''' Args: +''' FileName: A string representing a zip file +''' Destination: The folder where to extract the compressed files to. +''' Existing files with the same names are overwritten. Other files stay untouched. +''' When the folder does not exist, it is created, as are subfolders. +''' Source: A single string or an array of strings with valid names of files contained in the zip file +''' When Source is omitted, is an empty array or an empty string, all the files and folders are extracted. +''' Returns: +''' True when successful. +''' When False, an error message might be found in the console. +''' Exceptions: +''' UNKNOWNFILEERROR The file does not exist or is a folder +''' FILESYSTEMERROR The method is not applicable on document's file systems +''' Example: +''' a = SF_FileSystem.ExtractFromZip("C:\writerdoc.odt", Source := Array("content.xml", "meta.xml"), Desctination := "C:\TEMP") + +Dim bExtract As Boolean ' Return value +Dim vExtract As Variant ' Returned from Python (True/False, error message) +Dim vSource As Variant ' Alias of Source +Const cstPyHelper = "$" & "_SF_FileSystem__ExtractFromZip" +Const cstThisSub = "FileSystem.ExtractFromZip" +Const cstSubArgs = "FileName, Destination, [Source=""""]" + + If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch + bExtract = False + +Check: + If IsMissing(Source) Or IsEmpty(Source) Then Source = "" + If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then + If Not SF_Utils._ValidateFile(FileName, "FileName") Then GoTo Finally + If Not SF_Utils._ValidateFile(Destination, "Destination") Then GoTo Finally + If IsArray(Source) Then + If Not SF_Utils._ValidateArray(Source, "Source", 1, V_STRING) Then GoTo Finally + Else + If Not SF_Utils._Validate(Source, "Source", V_STRING) Then GoTo Finally + End If + End If + If SF_FileSystem._IsDocFileSystem(FileName) Then GoTo CatchNotSupported + +Try: + If SF_FileSystem.FileExists(FileName) Then + With ScriptForge.SF_Session + ' Be sure to pass an array to the Python script + If Not IsArray(Source) Then + If Len(Source) = 0 Then vSource = Array() Else vSource = Array(Source) + Else + vSource = Source + End If + vExtract = .ExecutePythonScript(.SCRIPTISSHARED, _SF_.PythonHelper & cstPyHelper _ + , _ConvertFromUrl(FileName), Destination, vSource) + bExtract = vExtract(0) + If Not bExtract Then SF_Exception.DebugPrint("ExtractFromZip", vExtract(1)) + End With + Else + GoTo CatchNotExists + End If + +Finally: + ExtractFromZip = bExtract + SF_Utils._ExitFunction(cstThisSub) + Exit Function +Catch: + GoTo Finally +CatchNotExists: + SF_Exception.RaiseFatal(UNKNOWNFILEERROR, "FileName", FileName) + GoTo Finally +CatchNotSupported: + SF_Exception.RaiseFatal(FILESYSTEMERROR, "FileName", Split(cstThisSub, ".")(1), FileName) + GoTo Finally +End Function ' ScriptForge.SF_FileSystem.ExtractFromZip + REM ----------------------------------------------------------------------------- Public Function FileExists(Optional ByVal FileName As Variant) As Boolean ''' Return True if the given file exists @@ -829,6 +1006,74 @@ CatchFolder: GoTo Finally End Function ' ScriptForge.SF_FileSystem.Files +REM ----------------------------------------------------------------------------- +Public Function FilesInZip(Optional ByVal FileName As Variant _ + , Optional ByVal Pattern As Variant _ + ) As Variant +''' Returns The list of the files contained in a zip container. +''' The list of names may optionally be filtered. +''' The method is not supported for document's internal file systems. +''' Args: +''' FileName: A string representing a zip file +''' Pattern: A pattern as a not case-sensitive string containing 0 or more wildcards +''' Admitted wildcard are: the "?" represents any single character +''' the "*" represents zero, one, or multiple characters +''' Returns: +''' An unordered array of file names without a heading slash. +''' The path separator is always the slash ("/"), also on Windows. +''' The list may be filtered with a name pattern. Without pattern, all files are returned. +''' The resulting array may be empty. +''' An empty array is returned when the file is not a valid zip file. +''' Exceptions: +''' UNKNOWNFILEERROR The file does not exist or is a folder +''' FILESYSTEMERROR The method is not applicable on document's file systems +''' Example: +''' a = SF_FileSystem.FilesInZip("C:\writerdoc.odt", Pattern := "*.xml") + +Dim vFiles As Variant ' Return value +Const cstPyHelper = "$" & "_SF_FileSystem__FilesInZip" +Const cstThisSub = "FileSystem.FilesInZip" +Const cstSubArgs = "FileName, [Pattern=""""]" + + If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch + vFiles = Array() + +Check: + If IsMissing(Pattern) Or IsEmpty(Pattern) Then Pattern = "" + If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then + If Not SF_Utils._ValidateFile(FileName, "FileName") Then GoTo Finally + If Not SF_Utils._Validate(Pattern, "Pattern", V_STRING) Then GoTo Finally + End If + If SF_FileSystem._IsDocFileSystem(FileName) Then GoTo CatchNotSupported + +Try: + If SF_FileSystem.FileExists(FileName) Then + With ScriptForge.SF_Session + vFiles = .ExecutePythonScript(.SCRIPTISSHARED, _SF_.PythonHelper & cstPyHelper _ + , _ConvertFromUrl(FileName)) + End With + ' Filter files based on pattern + If Len(Pattern) > 0 And UBound(vFiles) > -1 Then + vFiles = ScriptForge.SF_Array.Filter(ScriptForge.SF_String, "IsLike", vFiles, Pattern, False) + End If + Else + GoTo CatchNotExists + End If + +Finally: + FilesInZip = vFiles + SF_Utils._ExitFunction(cstThisSub) + Exit Function +Catch: + GoTo Finally +CatchNotExists: + SF_Exception.RaiseFatal(UNKNOWNFILEERROR, "FileName", FileName) + GoTo Finally +CatchNotSupported: + SF_Exception.RaiseFatal(FILESYSTEMERROR, "FileName", Split(cstThisSub, ".")(1), FileName) + GoTo Finally +End Function ' ScriptForge.SF_FileSystem.FilesInZip + REM ----------------------------------------------------------------------------- Public Function FolderExists(Optional ByVal FolderName As Variant) As Boolean ''' Return True if the given folder name exists @@ -1293,12 +1538,65 @@ CatchNotSupported: GoTo Finally End Function ' ScriptForge.SF_FileSystem.HashFile +REM ----------------------------------------------------------------------------- +Public Function IsZipFile(Optional ByVal FileName As Variant) As Boolean +''' Returns True if filename is a valid ZIP file based on its magic number. +''' The method is not supported for document's internal file systems. +''' Args: +''' FileName: a string representing a file +''' Returns: +''' True when FileName is a valid zip file. Otherwise False. +''' Exceptions: +''' UNKNOWNFILEERROR The file does not exist or is a folder +''' FILESYSTEMERROR The method is not applicable on document's file systems +''' Example: +''' MsgBox SF_FileSystem.IsZipFile("C:\writerdoc.odt") + +Dim bIsZip As Boolean ' Return value +Const cstPyHelper = "$" & "_SF_FileSystem__IsZipFile" +Const cstThisSub = "FileSystem.IsZipFile" +Const cstSubArgs = "FileName" + + If SF_Utils._ErrorHandling() Then On Local Error GoTo Catch + bIsZip = False + +Check: + If SF_Utils._EnterFunction(cstThisSub, cstSubArgs) Then + If Not SF_Utils._ValidateFile(FileName, "FileName") Then GoTo Finally + End If + If SF_FileSystem._IsDocFileSystem(FileName) Then GoTo CatchNotSupported + +Try: + If SF_FileSystem.FileExists(FileName) Then + With ScriptForge.SF_Session + bIsZip = .ExecutePythonScript(.SCRIPTISSHARED, _SF_.PythonHelper & cstPyHelper _ + , _ConvertFromUrl(FileName)) + End With + Else + GoTo CatchNotExists + End If + +Finally: + IsZipFile = bIsZip + SF_Utils._ExitFunction(cstThisSub) + Exit Function +Catch: + GoTo Finally +CatchNotExists: + SF_Exception.RaiseFatal(UNKNOWNFILEERROR, "FileName", FileName) + GoTo Finally +CatchNotSupported: + SF_Exception.RaiseFatal(FILESYSTEMERROR, "FileName", Split(cstThisSub, ".")(1), FileName) + GoTo Finally +End Function ' ScriptForge.SF_FileSystem.IsZipFile + REM ----------------------------------------------------------------------------- Public Function Methods() As Variant ''' Return the list or methods of the FileSystem service as an array Methods = Array("BuildPath" _ , "CompareFiles" _ + , "CompressToZip" _ , "CopyFile" _ , "CopyFolder" _ , "CreateFolder" _ @@ -1306,8 +1604,10 @@ Public Function Methods() As Variant , "DeleteFile" _ , "DeleteFolder" _ , "ExtensionFolder" _ + , "ExtractFromZip" _ , "FileExists" _ , "Files" _ + , "FilesInZip" _ , "FolderExists" _ , "GetBaseName" _ , "GetExtension" _ @@ -1317,6 +1617,7 @@ Public Function Methods() As Variant , "GetParentFolderName" _ , "GetTempName" _ , "HashFile" _ + , "IsZipFile" _ , "MoveFile" _ , "MoveFolder" _ , "Normalize" _ diff --git a/wizards/source/scriptforge/python/ScriptForgeHelper.py b/wizards/source/scriptforge/python/ScriptForgeHelper.py index f2fe8db8aebc..5604df51bf5f 100644 --- a/wizards/source/scriptforge/python/ScriptForgeHelper.py +++ b/wizards/source/scriptforge/python/ScriptForgeHelper.py @@ -22,26 +22,28 @@ # distributed with this file, see http://www.gnu.org/licenses/ . """ -Collection of Python helper functions called from the ScriptForge Basic libraries -to execute specific services that are not or not easily available from Basic directly. -When relevant, the methods present in the ScriptForge Python module (scriptforge.py) might call the -functions below for compatibility reasons. -""" + Collection of Python helper functions called from the ScriptForge Basic libraries + to execute specific services that are not or not easily available from Basic directly. + When relevant, the methods present in the ScriptForge Python module (scriptforge.py) may call the + functions below directly to bypass the Python-Basic bridge. + """ import getpass import os import platform import hashlib import filecmp +import pathlib +import zipfile import webbrowser import json class _Singleton(type): """ - A Singleton design pattern - Credits: « Python in a Nutshell » by Alex Martelli, O'Reilly - """ + A Singleton design pattern + Credits: « Python in a Nutshell » by Alex Martelli, O'Reilly + """ instances = {} def __call__(cls, *args, **kwargs): @@ -57,8 +59,8 @@ class _Singleton(type): def _SF_Dictionary__ConvertToJson(propval, indent = None) -> str: # used by Dictionary.ConvertToJson() Basic method """ - Given an array of PropertyValues as argument, convert it to a JSON string - """ + Given an array of PropertyValues as argument, convert it to a JSON string + """ # Array of property values => Dict(ionary) => JSON pvDict = {} for pv in propval: @@ -66,11 +68,12 @@ def _SF_Dictionary__ConvertToJson(propval, indent = None) -> str: return json.dumps(pvDict, indent=indent, skipkeys=True) -def _SF_Dictionary__ImportFromJson(jsonstr: str): # used by Dictionary.ImportFromJson() Basic method - """ - Given a JSON string as argument, convert it to a list of tuples (name, value) - The value must not be a (sub)dict. This doesn't pass the python-basic bridge. +def _SF_Dictionary__ImportFromJson(jsonstr: str) -> list: + # used by Dictionary.ImportFromJson() Basic method """ + Given a JSON string as argument, convert it to a list of tuples (name, value) + The value must not be a (sub)dict. This doesn't pass the python-basic bridge. + """ # JSON => Dictionary => Array of tuples/lists dico = json.loads(jsonstr) result = [] @@ -94,9 +97,9 @@ def _SF_Dictionary__ImportFromJson(jsonstr: str): # used by Dictionary.ImportFr def _SF_Exception__PythonPrint(string: str) -> bool: # used by SF_Exception.PythonPrint() Basic method """ - Write the argument to stdout. - If the APSO shell console is active, the argument will be displayed in the console window - """ + Write the argument to stdout. + If the APSO shell console is active, the argument will be displayed in the console window + """ print(string) return True @@ -108,27 +111,84 @@ def _SF_Exception__PythonPrint(string: str) -> bool: def _SF_FileSystem__CompareFiles(filename1: str, filename2: str, comparecontents=True) -> bool: # used by SF_FileSystem.CompareFiles() Basic method """ - Compare the 2 files, returning True if they seem equal, False otherwise. - By default, only their signatures (modification time, ...) are compared. - When comparecontents == True, their contents are compared. - """ + Compare the 2 files, returning True if they seem equal, False otherwise. + By default, only their signatures (modification time, ...) are compared. + When comparecontents == True, their contents are compared. + """ try: return filecmp.cmp(filename1, filename2, not comparecontents) except Exception: return False -def _SF_FileSystem__GetFilelen(systemfilepath: str) -> str: # used by SF_FileSystem.GetFilelen() Basic method +def _SF_FileSystem__CompressToZip(filename: str, mode: str, folder: str, rootfolder: str = None) -> tuple[bool, str]: + # used by SF_FileSystem.CompressToZip() Basic method + """ + Stores the content of folder, including its subfolders, into a zip archive. + The mode argument determines if the zip file is created (e'x'clusive create) or not ('a'ppend). + The compressed files are stored optionally under a root folder. + """ + try: + if mode == 'a': # existence of filename is checked in the calling routine + if not zipfile.is_zipfile(filename): + return False, "File is not a zip archive: '%s'" % filename + directory = pathlib.Path(folder) + # Write compressed files under root or under the given root folder. Normal slashes are required, also in Windows.. + topfolder = (None if rootfolder is None else pathlib.PurePosixPath(rootfolder + '/')) + with zipfile.ZipFile(filename, mode, strict_timestamps = False) as archive: + for filepath in directory.rglob('*'): + name = (filepath.relative_to(directory) if topfolder is None + else topfolder.joinpath(filepath.relative_to(directory))) + archive.write(filepath, arcname = name, compress_type = zipfile.ZIP_DEFLATED) + return True, '' + except Exception as e: + return False, str(e) + + +def _SF_FileSystem__ExtractFromZip(filename: str, destination: str, source: tuple = ()) -> tuple[bool, str]: + # used by SF_FileSystem.ExtractFromZip() Basic method + """ + Stores the source file(s) found in the given ZIP file into a destination folder. + """ + try: + if not zipfile.is_zipfile(filename): + return False, "File is not a zip archive: '%s'" % filename + with zipfile.ZipFile(filename, mode = 'r') as archive: + archive.extractall(destination, members = (None if len(source) == 0 else source)) + return True, '' + except Exception as e: + return False, str(e) + + +def _SF_FileSystem__FilesInZip(filename: str) -> list: + # used by SF_FileSystem.FilesInZip() Basic method + """ + Returns a list of archive members by name. The objects are in the same order as their entries + in the given ZIP file. + """ + try: + if not zipfile.is_zipfile(filename): + return [] + with zipfile.ZipFile(filename, mode = 'r') as archive: + namelist = archive.namelist() + return namelist + except Exception: + return [] + + +def _SF_FileSystem__GetFilelen(systemfilepath: str) -> str: + # used by SF_FileSystem.GetFilelen() Basic method return str(os.path.getsize(systemfilepath)) -def _SF_FileSystem__HashFile(filename: str, algorithm: str) -> str: # used by SF_FileSystem.HashFile() Basic method - """ - Hash a given file with the given hashing algorithm - cfr. https://www.pythoncentral.io/hashing-files-with-python/ - Example - hash = _SF_FileSystem__HashFile('myfile.txt','MD5') +def _SF_FileSystem__HashFile(filename: str, algorithm: str) -> str: + # used by SF_FileSystem.HashFile() Basic method """ + Hash a given file with the given hashing algorithm + cfr. https://www.pythoncentral.io/hashing-files-with-python/ + Example + hash = _SF_FileSystem__HashFile('myfile.txt','MD5') + """ algo = algorithm.lower() try: if algo in hashlib.algorithms_guaranteed: @@ -159,13 +219,21 @@ def _SF_FileSystem__HashFile(filename: str, algorithm: str) -> str: # used by S return '' +def _SF_FileSystem__IsZipFile(filename: str) -> bool: + # used by SF_FileSystem.IsZipFile() Basic method + """ + Returns True if filename is a valid ZIP file based on its magic number, otherwise returns False. + """ + return zipfile.is_zipfile(filename) + + def _SF_FileSystem__Normalize(systemfilepath: str) -> str: # used by SF_FileSystem.Normalize() Basic method """ - Normalize a pathname by collapsing redundant separators and up-level references so that - A//B, A/B/, A/./B and A/foo/../B all become A/B. - On Windows, it converts forward slashes to backward slashes. - """ + Normalize a pathname by collapsing redundant separators and up-level references so that + A//B, A/B/, A/./B and A/foo/../B all become A/B. + On Windows, it converts forward slashes to backward slashes. + """ return os.path.normpath(systemfilepath) @@ -175,8 +243,8 @@ def _SF_FileSystem__Normalize(systemfilepath: str) -> str: def _SF_Platform(propertyname: str): # used by SF_Platform Basic module """ - Switch between SF_Platform properties (read the documentation about the ScriptForge.Platform service) - """ + Switch between SF_Platform properties (read the documentation about the ScriptForge.Platform service) + """ pf = Platform() if propertyname == 'Architecture': return pf.Architecture @@ -249,8 +317,8 @@ class Platform(object, metaclass = _Singleton): def _SF_Session__OpenURLInBrowser(url: str): # Used by SF_Session.OpenURLInBrowser() Basic method """ - Display url using the default browser - """ + Display url using the default browser + """ try: webbrowser.open(url, new = 2) except Exception: # whatever happened, continue the execution ... @@ -264,10 +332,10 @@ def _SF_Session__OpenURLInBrowser(url: str): # Used by SF_Session.OpenURLInBr def _SF_String__HashStr(string: str, algorithm: str) -> str: # used by SF_String.HashStr() Basic method """ - Hash a given UTF-8 string with the given hashing algorithm - Example - hash = _SF_String__HashStr('This is a UTF-8 encoded string.','MD5') - """ + Hash a given UTF-8 string with the given hashing algorithm + Example + hash = _SF_String__HashStr('This is a UTF-8 encoded string.','MD5') + """ algo = algorithm.lower() try: if algo in hashlib.algorithms_guaranteed: @@ -301,6 +369,7 @@ def _SF_String__HashStr(string: str, algorithm: str) -> str: # used by SF_Strin g_exportedScripts = () if __name__ == "__main__": + """ print(_SF_Platform('Architecture')) print(_SF_Platform('ComputerName')) print(_SF_Platform('CPUCount')) @@ -323,10 +392,13 @@ if __name__ == "__main__": # _SF_Session__OpenURLInBrowser('https://docs.python.org/3/library/webbrowser.html') # js = """ - {"firstName": "John","lastName": "Smith","isAlive": true,"age": 27, + """{"firstName": "John","lastName": "Smith","isAlive": true,"age": 27, "address": {"streetAddress": "21 2nd Street","city": "New York","state": "NY","postalCode": "10021-3100"}, "phoneNumbers": [{"type": "home","number": "212 555-1234"},{"type": "office","number": "646 555-4567"}], "children": ["Q", "M", "G", "T"],"spouse": null} """ - arr = _SF_Dictionary__ImportFromJson(js) - print(arr) + """arr = _SF_Dictionary__ImportFromJson(js) + print(arr)""" + a = "/tmp/SF_YYY/zipped.odt" + b = _SF_FileSystem__CompressToZip(a, "a", "/tmp/SF_XXX", "New") + print(b) \ No newline at end of file diff --git a/wizards/source/scriptforge/python/scriptforge.py b/wizards/source/scriptforge/python/scriptforge.py index 2f54e5462f0a..cf86a1ca2e09 100644 --- a/wizards/source/scriptforge/python/scriptforge.py +++ b/wizards/source/scriptforge/python/scriptforge.py @@ -1220,6 +1220,9 @@ class SFScriptForge: else: return False + def CompressToZip(self, filename, foldername, rootfolder = ''): + return self.ExecMethod(self.vbMethod, 'CompressToZip', filename, foldername, rootfolder) + def CopyFile(self, source, destination, overwrite = True): return self.ExecMethod(self.vbMethod, 'CopyFile', source, destination, overwrite) @@ -1241,12 +1244,18 @@ class SFScriptForge: def ExtensionFolder(self, extension): return self.ExecMethod(self.vbMethod, 'ExtensionFolder', extension) + def ExtractFromZip(self, filename, destination, source = ''): + return self.ExecMethod(self.vbMethod, 'ExtractFromZip', filename, destination, source) + def FileExists(self, filename): return self.ExecMethod(self.vbMethod, 'FileExists', filename) def Files(self, foldername, filter = '', includesubfolders = False): return self.ExecMethod(self.vbMethod, 'Files', foldername, filter, includesubfolders) + def FilesInZip(self, filename, pattern = ''): + return self.ExecMethod(self.vbMethod, 'FilesInZip', filename, pattern) + def FolderExists(self, foldername): return self.ExecMethod(self.vbMethod, 'FolderExists', foldername) @@ -1284,6 +1293,14 @@ class SFScriptForge: else: return '' + def IsZipFile(self, filename): + py = ScriptForge.pythonhelpermodule + '$' + '_SF_FileSystem__IsZipFile' + if self.FileExists(filename): + file = self._ConvertFromUrl(filename) + return self.SIMPLEEXEC(py, file) + else: + return False + def MoveFile(self, source, destination): return self.ExecMethod(self.vbMethod, 'MoveFile', source, destination) diff --git a/wizards/source/scriptforge/python/scriptforge.pyi b/wizards/source/scriptforge/python/scriptforge.pyi index 444863a5da55..ed29d33944e9 100644 --- a/wizards/source/scriptforge/python/scriptforge.pyi +++ b/wizards/source/scriptforge/python/scriptforge.pyi @@ -975,6 +975,26 @@ class SFScriptForge: """ ... + def CompressToZip(self, filename: FILE, foldername: FILE, rootfolder: str = ...) -> bool: + """ + Stores the content of Folder, including its subfolders, into a zip archive. + Encrypted files are not supported. The compressed files are stored optionally under a root folder. + Files existing already in the archive are not overwritten. + Args + ``filename``: a string representing the zip file. + If it does not exist, the archive is created, oncluding its parent folders. + All files present in the archive are kept untouched. + + ``foldername``: the folder to compress, as a string. All files and subfolders are included. + The folder itself is not. + + ``rootfolder``: when absent, files and folders are loaded in the archive from the root ("/"). + When present, given as a string without path separator, they are stored below that folder. + Returns + ``True`` when successful. When ``False``, the reason might be found in the console. + """ + ... + def CopyFile(self, source: FILE, destination: FILE, overwrite: bool = ...) -> bool: """ Copies one or more files from one location to another. @@ -1092,6 +1112,24 @@ class SFScriptForge: """ ... + def ExtractFromZip(self, filename: FILE, destination: FILE, source: str | tuple[str, ...] = ...) -> bool: + """ + Extracts a list of files contained in a zip archive and stores them in a folder. + Args + ``filename``: a string representing the zip archive. + + ``destination``: the folder where to extract the compressed files to. + Existing files with the same names are overwritten without warning. Other files stay untouched. + When the folder or a subfolder does not exist, it is created. + + ``source``: a single string or a tuple of strings with valid names of files contained in + the zip file. When Source is omitted, is an empty tuple or an empty string, + all the files and folders are extracted. + Returns + ``True`` when successful. When ``False``, the reason might be found in the console. + """ + ... + def FileExists(self, filename: FILE) -> bool: """ Return ``True`` if the given file exists. @@ -1121,6 +1159,24 @@ class SFScriptForge: """ ... + def FilesInZip(self, filename: FILE, pattern: str = ...) -> tuple[str, ...]: + """ + Returns The list of the files contained in a zip container. The list of names may optionally + be filtered. + Args + ``filename``: a string representing the zip archive. + + ``pattern``: a pattern as a not case-sensitive string containing 0 or more wildcards. + Admitted wildcard are: the "?" represents any single character, + the "*" represents zero, one, or multiple characters. + Returns + An unordered tuple of file names without a heading slash. The path separator is always + the slash ("/"), also on Windows. The list may be filtered with a name pattern. + Without pattern, all files are returned. The resulting tuple may be empty. An empty tuple + is returned when the file is not a valid zip file. + """ + ... + def FolderExists(self, foldername: FILE) -> bool: """ Returns ``True`` if the given folder name exists. @@ -1235,6 +1291,16 @@ class SFScriptForge: """ ... + def IsZipFile(self, filename: FILE) -> bool: + """ + Returns ``True`` if filename is a valid ZIP file based on its magic number. + Args + ``filename``: a string representing the zip archive. + Returns + ``True`` when ``filename`` is a valid zip file. Otherwise ``False``. + """ + ... + def MoveFile(self, source: FILE, destination: FILE) -> bool: """ Moves one or more files from one location to another.