For posterity's sake, I meant to come back here and post the solution a while ago. I've got the full story here in this blog post:
http://stephenhuey.wordpress.com/2010/01/01/docxgae/ The relatively new Microsoft Office formats ending with extensions such as .docx and .xlsx are zip files of directories of text files (XML) and images. Tried-and-true Java libraries for writing the old binary Microsoft Word and Excel formats aren't yet supported on Google App Engine, so I was glad to get this working since it was a required feature for our application. My modified code for the zipDir and addDir methods is below. As you can see, I'm closing the ZipOutputStream rather than the OutputStream: + + + + + + + + + + + + + + + + + + + + import java.io.BufferedInputStream; import java.io.IOException; import java.io.OutputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import org.apache.commons.vfs.FileObject; import org.apache.commons.vfs.FileSystemException; import org.apache.commons.vfs.FileSystemManager; import org.apache.commons.vfs.FileType; public class FileObjectHelper { public static FileObject createFolder(FileSystemManager fsManager, String absolutePath) throws FileSystemException { FileObject theFolder = fsManager.resolveFile( absolutePath ); if ( theFolder.exists() == false) { theFolder.createFolder(); } return theFolder; } public static FileObject createFile(FileSystemManager fsManager, String absolutePath) throws FileSystemException { FileObject theFile = fsManager.resolveFile( absolutePath ); if ( theFile.exists() == false) { theFile.createFile(); } return theFile; } public static void zipDir(FileObject docxZipFile, FileObject directoryToZip) throws IOException { OutputStream out = docxZipFile.getContent().getOutputStream(); ZipOutputStream zout = new ZipOutputStream(out); addDir(directoryToZip, zout, ""); zout.close(); // make sure you close the ZipOutputStream, not the OutputStream! } public static void addDir(FileObject dirObj, ZipOutputStream zout, String basePathSoFar) throws IOException { FileObject[] files = dirObj.getChildren(); byte[] tmpBuf = new byte[1024]; for (int i = 0; i < files.length; i++) { FileObject currentFile = files[i]; String currentFileBaseName = currentFile.getName().getBaseName(); if (currentFile.getType().equals(FileType.FOLDER)) { addDir(currentFile, zout, basePathSoFar + currentFileBaseName + "/"); } else { // else it's a file, not a directory BufferedInputStream bis = new BufferedInputStream(currentFile.getContent().getInputStream()); zout.putNextEntry(new ZipEntry(basePathSoFar + currentFileBaseName)); int len; while ((len = bis.read(tmpBuf)) != -1) { zout.write(tmpBuf, 0, len); } zout.closeEntry(); bis.close(); } // end if } // end for loop } // end addDir } + + + + + + + + + + + + + + + + + + + + Happy New Year, Stephen Huey On Tue, Dec 15, 2009 at 1:54 AM, Stephen Huey <[email protected]>wrote: > I'm using GaeVFS which is based on Commons VFS, and I feel that my question > is probably more relevant to Commons VFS than the Google App Engine layer on > top of it. My goal is to construct valid zip files on Google App Engine, > and I successfully used GaeVFS to create zip files that Mac OS X can open > and that Winzip on Windows can open, but Windows XP Compressed Folders > refused to acknowledge files in subdirectories in the zip until I removed > the leading forward slash from a ZipEntry whose name began with a > directory. > > For example, the following basic class successfully generates a zip file > containing a file in a subdirectory that Windows XP Compressed Folders will > open: > > > import java.util.zip.*; > import java.io.*; > public class ZipThis { > public static void main(String args[]) throws IOException { > if (args.length < 1) { > System.err.println("usage: java ZipThis Zip.zip"); > System.exit(-1); > } > File zipFile = new File(args[0]); > if (zipFile.exists()) { > System.err.println("Zip file already exists, please try another"); > System.exit(-2); > } > FileOutputStream fos = new FileOutputStream(zipFile); > ZipOutputStream zos = new ZipOutputStream(fos); > int bytesRead; > String[] files = new String[] {"D:/stylessub/styles.xml", > "D:/document.xml"}; > String[] entryName = new String[] {"stylessub/styles.xml", > "document.xml"}; > byte[] buffer = new byte[1024]; > for (int i=0, n=files.length; i < n; i++) { > String name = files[i]; > File file = new File(name); > if (!file.exists()) { > System.out.println("Skipping: " + name); > continue; > } > BufferedInputStream bis = new BufferedInputStream(new > FileInputStream(file)); > ZipEntry entry = new ZipEntry(entryName[i]); > zos.putNextEntry(entry); > while ((bytesRead = bis.read(buffer)) != -1) { > zos.write(buffer, 0, bytesRead); > } > bis.close(); > zos.closeEntry(); > > } > zos.close(); > } > } > > > Now I'm trying to get my virtual file system code to generate a zip that > Windows won't complain about with this error message: "The Compressed > (zipped) Folder is isvalid or corrupted." I want to meet the Microsoft > employee who wrote that error message! :) > > So here's some initial setup in my servlet: > > FileObject docxZipFile = FileObjectHelper.createFile(fsManager, > "gae://gaevfs/generatedZip/docxFile.zip"); > FileObject docxRootFolder = FileObjectHelper.createFolder(fsManager, > "gae://gaevfs/docxDirectory"); > > These helper methods are being used: > > public static FileObject createFile(FileSystemManager fsManager, String > absolutePath) throws FileSystemException { > FileObject theFile = fsManager.resolveFile( absolutePath ); > if ( theFile.exists() == false) { > theFile.createFile(); > } > return theFile; > } > > public static FileObject createFolder(FileSystemManager fsManager, > String absolutePath) throws FileSystemException { > FileObject theFolder = fsManager.resolveFile( absolutePath ); > if ( theFolder.exists() == false) { > theFolder.createFolder(); > } > return theFolder; > } > > Pardon the near redundancy. Anyway, the next line begins the zipping: > > FileObjectHelper.zipDir(docxZipFile, docxRootFolder); > > > Here's the zipDir method: > > > public static void zipDir(FileObject docxZipFile, FileObject > directoryToZip) throws Exception { > OutputStream out = docxZipFile.getContent().getOutputStream(); > //ByteArrayOutputStream bos = new ByteArrayOutputStream(); > ZipOutputStream zout = new ZipOutputStream(out); > addDir(directoryToZip, zout, directoryToZip.getName().getBaseName() > + "/"); > out.close(); > //return bos.toByteArray(); > } > > > As you can see, I played with pushing down a byte array of the resulting > zip file on the servlet output stream instead of saving the generated zip > file to the virtual file system, but that didn't make my error message go > away. Here's the addDir method: > > > public static void addDir(FileObject dirObj, ZipOutputStream out, > String basePathSoFar) throws IOException { > System.out.println("addDir for " + dirObj.getName()); > FileObject[] files = dirObj.getChildren(); > byte[] tmpBuf = new byte[1024]; > > for (int i = 0; i < files.length; i++) { > FileObject currentFile = files[i]; > System.out.println("currentFile is " + > currentFile.getName().getBaseName()); > if (currentFile.getType().equals(FileType.FOLDER)) { > addDir(currentFile, out, basePathSoFar + > currentFile.getName().getBaseName() + "/"); > > } else { > > System.out.println("making entry for: " + basePathSoFar + > currentFile.getName().getBaseName()); > InputStream in = currentFile.getContent().getInputStream(); > out.putNextEntry(new ZipEntry(basePathSoFar + > currentFile.getName().getBaseName())); > int len; > while ((len = in.read(tmpBuf)) != -1) { > out.write(tmpBuf, 0, len); > } > out.closeEntry(); > in.close(); > } > } // end for loop > } > > > I used to have the buffer read check for greater than zero rather than not > equal to negative one. Anyway, for some reason this doesn't work even > though it seems very close to the working program above. I'll paste the > print statements down below so you can see that it doesn't appear as if VFS > is putting any extra junk into the ZipEntry names or anything like that. > > I'd love to try out any bright ideas anyone has! > > Thanks so much... > > > > The server is running at http://localhost:8888/ > Passing the zip file: > /D:/workspace/my-app/war/gaevfs/generatedZip/docxFile.zip > Writing this zip file: docxFile.zip > Zipping up directory: docxDirectory > addDir for gae:///D:/workspace/my-app/war/gaevfs/docxDirectory > currentFile is _rels > addDir for gae:///D:/workspace/my-app/war/gaevfs/docxDirectory/_rels > currentFile is docProps > addDir for gae:///D:/workspace/my-app/war/gaevfs/docxDirectory/docProps > currentFile is app.xml > making entry for: docxDirectory/docProps/app.xml > currentFile is core.xml > making entry for: docxDirectory/docProps/core.xml > currentFile is word > addDir for gae:///D:/workspace/my-app/war/gaevfs/docxDirectory/word > currentFile is document.xml > making entry for: docxDirectory/word/document.xml > currentFile is fontTable.xml > making entry for: docxDirectory/word/fontTable.xml > currentFile is settings.xml > making entry for: docxDirectory/word/settings.xml > currentFile is styles.xml > making entry for: docxDirectory/word/styles.xml > currentFile is webSettings.xml > making entry for: docxDirectory/word/webSettings.xml > currentFile is _rels > addDir for gae:///D:/workspace/my-app/war/gaevfs/docxDirectory/word/_rels > currentFile is document.xml.rels > making entry for: docxDirectory/word/_rels/document.xml.rels > currentFile is media > addDir for gae:///D:/workspace/my-app/war/gaevfs/docxDirectory/word/media > currentFile is image1.jpeg > making entry for: docxDirectory/word/media/image1.jpeg > currentFile is image2.jpeg > making entry for: docxDirectory/word/media/image2.jpeg > currentFile is theme > addDir for gae:///D:/workspace/my-app/war/gaevfs/docxDirectory/word/theme > currentFile is theme1.xml > making entry for: docxDirectory/word/theme/theme1.xml > currentFile is [Content_Types].xml > making entry for: docxDirectory/[Content_Types].xml > Passing the zip file: > /D:/workspace/my-app/war/gaevfs/generatedZip/betterzip.zip > Writing this zip file: betterzip.zip > Zipping up directory: docxDirectory > addDir for gae:///D:/workspace/my-app/war/gaevfs/docxDirectory > currentFile is _rels > addDir for gae:///D:/workspace/my-app/war/gaevfs/docxDirectory/_rels > currentFile is docProps > addDir for gae:///D:/workspace/my-app/war/gaevfs/docxDirectory/docProps > currentFile is app.xml > making entry for: docxDirectory/docProps/app.xml > currentFile is core.xml > making entry for: docxDirectory/docProps/core.xml > currentFile is word > addDir for gae:///D:/workspace/my-app/war/gaevfs/docxDirectory/word > currentFile is document.xml > making entry for: docxDirectory/word/document.xml > currentFile is fontTable.xml > making entry for: docxDirectory/word/fontTable.xml > currentFile is settings.xml > making entry for: docxDirectory/word/settings.xml > currentFile is styles.xml > making entry for: docxDirectory/word/styles.xml > currentFile is webSettings.xml > making entry for: docxDirectory/word/webSettings.xml > currentFile is _rels > addDir for gae:///D:/workspace/my-app/war/gaevfs/docxDirectory/word/_rels > currentFile is document.xml.rels > making entry for: docxDirectory/word/_rels/document.xml.rels > currentFile is media > addDir for gae:///D:/workspace/my-app/war/gaevfs/docxDirectory/word/media > currentFile is image1.jpeg > making entry for: docxDirectory/word/media/image1.jpeg > currentFile is image2.jpeg > making entry for: docxDirectory/word/media/image2.jpeg > currentFile is theme > addDir for gae:///D:/workspace/my-app/war/gaevfs/docxDirectory/word/theme > currentFile is theme1.xml > making entry for: docxDirectory/word/theme/theme1.xml > currentFile is [Content_Types].xml > making entry for: docxDirectory/[Content_Types].xml > >
