1

I'm attempting to zip a folder containing subfolders and items, using Windows shell CopyHere command:

https://msdn.microsoft.com/en-us/library/windows/desktop/bb787866(v=vs.85).aspx https://msdn.microsoft.com/en-us/library/windows/desktop/ms723207(v=vs.85).aspx

Update: Note, prefer a native solution-- this is for a distributed Excel VBA tool, so bundling 3rd-party files is not ideal. And, need synchronous compression.

I can easily add a folder and its contents to the zip:

oShell.Namespace(sZipPath).CopyHere "C:\My Folder"

So we know CopyHere can process multiple objects inside a folder in 1 statement.

The problem is, the above command puts the containing-folder at the root of the zip, and it's contents inside of it. But, i don't want the containing folder-- just its contents.

The doc mentions a wildcard (option 128), but when i use a wildcard, i get an error:

oShell.Namespace(sZipPath).CopyHere "C:\My Folder\*"

The file name you specified is not valid or too long.

Perhaps there's a way to use my 1st command above, and then move the items in the zip to the root of the zip?

It would be acceptable to loop through each item in the source folder, adding one at a time to the zip. But, because CopyHere is asynchronous, each subsequent CopyHere fails if the previous CopyHere is not finished. None of the fixes work for this issue:

  • Comparing number of items in source-folder and destination-zip fail, because if the zip contains a folder, that counts as only 1 item (the items it contains are not counted. https://stackoverflow.com/a/16603850/209942

  • Waiting a while between each item works, but a timer is unacceptable: it's arbitrary. I cannot guess in advance the size or compress-time of each object.

  • Checking to see if the zip is locked for access failed for me. If I block my loop until the file is not locked, I still get a file-access error. https://stackoverflow.com/a/6666663/209942


Function FileIsOpen(sPathname As String) As Boolean ' true if file is open
    Dim lFileNum As Long
    lFileNum = FreeFile
    Dim lErr As Long
    On Error Resume Next
    Open sPathname For Binary Access Read Write Lock Read Write As #lFileNum
    lErr = Err
    Close #lFileNum
    On Error GoTo 0
    FileIsOpen = (lErr <> 0)
End Function

Update: VBA can call shell commands synchronously (instead of creating a shell32.shell object in VBA), so if CopyHere works on command-line or PowerShell, that could be the solution. Investigating...

Community
  • 1
  • 1
johny why
  • 2,047
  • 7
  • 27
  • 52
  • I'd say your best option is to stick to `CopyHere` and add a loop to wait until everything was added to the zip archive. It shouldn't be too difficult to wrap the code from my answer [here](http://stackoverflow.com/a/22050803/1630171) in a function. – Ansgar Wiechers Oct 01 '16 at 13:57
  • thx, @AnsgarWiechers, but as i noted in my OP, "Comparing number of items fails, because if the zip contains a folder, that counts as only 1 item --items inside the folder are not counted." That, and i'm not fond of polling loops. I have solved this issue, and posted a superior (for me) solution below. – johny why Oct 01 '16 at 16:59
  • Only the number of the immediate child items (files and folders) is relevant, not the number of items nested below that level. I tested the code from my answer with that exact scenario (compressing a folder containing files and a subfolder with some rather large files), and it worked as it should (meaning the wait loop terminated only after all child elements of that subfolder had been added). – Ansgar Wiechers Oct 01 '16 at 17:03
  • @AnsgarWiechers, thx for that. That's good info! However, i do not want to compress a folder containing items-- i only want to compress the contained items. For that, CopyHere would require me to individually add each subitem (folder or file), and compare item counts after that. Thus, CopyHere requires a loop (polling for item-count) *within a loop* (adding each sub-item). The solution i found adds all items in a single statement, no looping or polling or counting needed. – johny why Oct 01 '16 at 17:09
  • Retracting the edit after reading [Difference between Visual Basic 6.0 and VBA](//stackoverflow.com/q/993300); the difference is too small to care. – Martijn Pieters Oct 03 '16 at 14:02
  • thx, @MartijnPieters – johny why Oct 03 '16 at 14:05

2 Answers2

1

Automating Shell objects really isn't a viable approach as you have already discovered. The Explorer Shell doesn't really expose this capability in any other manner though, at least not before Windows Vista and then not in any fashion easily used from VB6 programs or VBA macros.

Your best bet is a 3rd party ActiveX library, but be careful about 64-bit VBA hosts where you'll need a 64-bit version of such a library.

Another option is to acquire a later copy of the zlibwapi.dll and use some VB6 wrapper code with it. This is also a 32-bit solution.

That's what Zipper & ZipWriter, Zipping from VB programs does. Considering your requirements (which for some reason includes a fear of the Timer control) you could use the synchronous ZipperSync Class. See post #4 there. That code includes a simple AddFolderToZipperSync bundling up the logic to add a folder instead of just a single file.

The downside of the synchronous class is that a large archival operation freezes your program UI until it completes. If you don't want that use the Zipper UserControl instead.

You could also take the ideas from that to write your own wrapper class.

Bob77
  • 13,167
  • 1
  • 29
  • 37
  • Thx for options. Not "fear" of timer -i can't predict compress-time, so it's just a guess. Amiright? "downside of the synchronous class ...large archival operation freezes your program" -i *want* synchronous. But hoping for a native solution (added to OP)-- this is a distrib'd Excel tool, so not ideal to require 3rd party components. Adding a reference in VBA to zlibwapi.dll throws error. ZipperSync adds quite a bit of code to my VB + 2 files (if i understand). If i must bundle a 3rd party, 7z.exe gives a simple 1-liner shell command, i can do synchronously: http://superuser.com/a/94437 – johny why Sep 30 '16 at 14:04
1

Solution:

Windows contains another native compression utility: CreateFromDirectory at a PowerShell prompt.

https://msdn.microsoft.com/en-us/library/system.io.compression.zipfile.createfromdirectory(v=vs.110).aspx

https://blogs.technet.microsoft.com/heyscriptingguy/2015/03/09/use-powershell-to-create-zip-archive-of-folder/

This requires .Net 4.0 or later:

> Add-Type -AssemblyName System.IO.Compression
> $src = "C:\Users\v1453957\documents\Experiment\rezip\aFolder"
> $zip="C:\Users\v1453957\Documents\Experiment\rezip\my.zip"
> [io.compression.zipfile]::CreateFromDirectory($src, $zip)

Note, you may have to provide the complete pathnames-- active directory was not implicit on my machine.

The above compression is synchronous at the PowerShell prompt, as the OP requests.


Next step is executing synchronously from VBA. The solution there is the .Run method in Windows Script Host Object Model. In VBA, set a reference to that, and do the following, setting the 3rd parameter of .Run command, bWaitOnReturn to True:

Function SynchronousShell(sCmd As String)As Long Dim oWSH As New IWshRuntimeLibrary.WshShell ShellSynch = oWSH.Run(sCmd, 3, True) Set oWSH = Nothing End Function

Now call SynchronousShell, and pass it the entire compression script.

I believe the only way for this process to work is if CreateFromDirectory is executed in the same session as Add-Type.

So, we must pass the whole thing as 1 string. That is, load all 4 commands into a single sCmd variable, so that Add-Type remains associated with the subsequent CreateFromDirectory. In PowerShell syntax, you can separate them with ;

https://thomas.vanhoutte.be/miniblog/execute-multiple-powershell-commands-on-one-line/

Also, you'll want to use single-quotes instead of double-quotes, else double quotes around the strings are removed when the daisy-chained commands are passed to powershell.exe

https://stackoverflow.com/a/39801732/209942

sCmd = "ps4 Add-Type -AssemblyName System.IO.Compression; $src = 'C:\Users\v1453957\documents\Experiment\rezip\aFolder'; $zip='C:\Users\v1453957\Documents\Experiment\rezip\my.zip'; [io.compression.zipfile]::CreateFromDirectory($src, $zip)"

Solved. The above constitutes the complete solution.


Extra info: Additional comments below are for special circumstances:

Multi-version .Net environments

If a .NET < 4.0 is the active environment on your OS, then System.IO.Compression does not exist-- the Add-Type command will fail. But if your machine has the .NET 4 assemblies available, you can still do this:

  • Create a batch file which runs PowerShell with .Net 4. See https://stackoverflow.com/a/31279372

  • In your Add-Type command above, use the exact path to the .Net 4 Compression assembly. On my Win Server 2008:

Add-Type -Path "C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.IO.Compression.FileSystem\v4.0_4.0.0.0__b77a5c561934e089\System.IO.Compression.FileSystem.dll"

Portability

Turns out, on my machine, I can copy the compression dll to any folder, and make calls to the copy and it works:

Add-Type -Path "C:\MyFunnyFolder\System.IO.Compression.FileSystem.dll"

I don't know what's required to ensure this works-- it might require the full .Net 4.0 or 2.0 files to be located in their expected directories. I assume the dll makes calls to other .Net assemblies. Maybe we just got lucky with this one :)

Character Limit

Depending on the depth of our paths and filenames, character-count may be a concern. PowerShell may have a 260-character limit (not sure).

https://support.microsoft.com/en-us/kb/830473

https://social.technet.microsoft.com/Forums/windowsserver/en-US/f895d766-5ffb-483f-97bc-19ac446da9f8/powershell-command-size-limit?forum=winserverpowershell

Since .Run goes through the Windows shell, you also have to worry about that character limit, but at 8k+, it's a bit roomier: https://blogs.msdn.microsoft.com/oldnewthing/20031210-00/?p=41553 https://stackoverflow.com/a/3205048/209942

Site below offers a 24k+ character method, but i've not studied it yet: http://itproctology.blogspot.com/2013/06/handling-freakishly-long-strings-from.html

At minimum, since we can put the dll wherever we like, we can put it in a folder near C: root-- keeping our character-count down.

Update: This post shows how we can put the whole thing in a script-file, and call it with ps4.cmd. This may become my preferred answer:

.\ps4.cmd GC .\zipper.ps1 | IEX

-- depending on answer here.


CopyHere:

Re the question: can CopyHere command execute on command-line?

CopyHere can be executed directly at PowerShell prompt (code below). However, even in powershell it's asynchronous-- control returns to PowerShell prompt before the process is finished. Therefore, no solution for the OP. Here's how it's done:

> $shellapp=new-object -com shell.application
> $zippath="test.zip"
> $zipobj=$shellapp.namespace((Get-Location).Path + "\$zippath")
> $srcpath="src"
> $srcobj=$shellapp.namespace((Get-Location).Path + "\$srcpath")
> $zipobj.Copyhere($srcobj.items())
Community
  • 1
  • 1
johny why
  • 2,047
  • 7
  • 27
  • 52