Objective-C-ZIPライブラリを用いて、フォルダ構造を保ったままzip化を行う


Objective-C-ZIPライブラリを用いて、フォルダ構造を保ったままzip化を行う

背景

  • POSIX関数のzip -rj ...でやろうとしたが、フォルダが含まれなくなるため不可。(全てのファイルがルートフォルダに設置される。)
    • ターミナル同様にcdを行って…とできれば実現できるのだけど。
  • zipでは限界がある(フォルダ構造が保てない等)ので、Objective-Zipライブラリを用いる必要に駆られた。
  • Objective-Zipという外部ライブラリを用いる。

期待される出力結果

  • 下記ファイル構造を保ったままzip化を行う。
  • 下記フォルダをダウンロードフォルダ直下に配置

  • 作成したzipファイルと解凍結果

CocoaPodsでのライブラリ追加

下記を参考に導入。

https://qiita.com/pekocalypse/items/b80f6c343355a872dde6
https://qiita.com/satoken0417/items/479bcdf91cff2634ffb1

Podfileは下記の通り。
platform :osx, '10.13.0'の指定でいいかは自信がない。

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
platform :osx, '10.13.0'
pod 'objective-zip', '~> 1.0'

target 'ObjectiveC_ZIP_LIB_TEST' do
  # Uncomment the next line if you're using Swift or would like to use dynamic frameworks
  # use_frameworks!

  # Pods for ObjectiveC_ZIP_LIB_TEST

end

GithubのREADMEの補足

Objective-Zipの使い方に関しては、基本READMEを参照する。
ただしGitHubの説明通りにzipファイルを作成すると解凍でエラーが発生する。(下記コードで行うとエラーが発生する)

    OZZipFile* zip = [[OZZipFile alloc] initWithFileName:@"test.zip" mode:OZZipFileModeCreate];
    OZZipWriteStream* write = [zip writeFileInZipWithName:@"hello.txt"
                                       compressionLevel:OZZipCompressionLevelBest];
    NSData* data = [@"hello" dataUsingEncoding:NSUTF8StringEncoding];
    [write writeData:data];
    [write finishedWriting];
    [zip close];

64bitモードに問題があるそうです。

You are having trouble with the 64 bit mode that objective-zip can use.

If you just add legacy32BitMode:YES when creating the archive, everything will be fine.

32bitモードを使用すると良いそうです。

OZZipFile *zipFile= [[OZZipFile alloc] initWithFileName:zipPath 
                                                   mode:OZZipFileModeCreate 
                                        legacy32BitMode:YES];

zip内のフォルダ構造を維持するには

フォルダ構造は各ファイルをzip化するときにパス指定で行うことで作成する。

File/folder hierarchy inide the zip
Please note that inside the zip files there is no representation of a file-folder hierarchy: it is simply embedded in file names (i.e.: a file with a name like "x/y/z/file.txt"). It is up to the program that extracts files to consider these file names as expressing a structure and rebuild it on the file system (and viceversa during creation). Common zippers/unzippers simply follow this rule.

例:relativeFilePath@"x/y/z/file.txt"を指定する。

OZZipWriteStream *zipWriteStream = [zip writeFileInZipWithName:relativeFilePath
                                                      compressionLevel:OZZipCompressionLevelBest];

作成したプログラム

冒頭部

#import "AppDelegate.h"
#import "Objective-Zip.h"
static NSString *const kTargetFolderPath = @"~/Downloads/SampleFolder"; // zip化対象フォルダ
static NSString *const kZipName          = @"result.zip";               // zip化する際のファイル名

呼び出し部

NSString *targetFolderPath = [kTargetFolderPath stringByExpandingTildeInPath];
[self zipFilesWithFolderURL:[NSURL fileURLWithPath:targetFolderPath] zipName:kZipName];

関数部

/**
 @brief フォルダ構造を保ったままフォルダ内のファイルのzip化を行う
 @param dirURL zip化対象フォルダ
 @param aZipName 作成するzip名
 */
- (void)zipFilesWithFolderURL:(NSURL *)dirURL zipName:(NSString *)aZipName {
    NSArray   *fileURLList = [self getFileURLListWithFolderURL:dirURL];
    NSString  *zipPath     = [NSBundle.mainBundle.bundlePath.stringByDeletingLastPathComponent stringByAppendingPathComponent:aZipName];
    OZZipFile *zip         = [[OZZipFile alloc] initWithFileName:zipPath
                                                            mode:OZZipFileModeCreate
                                                 legacy32BitMode:YES];
    for (NSURL *fileURL in fileURLList) {
        // zip内の構造を示すための、相対パスを作成する
        NSMutableString *relativeFilePath = [[NSMutableString alloc] initWithString:fileURL.path];
        NSRange range = [relativeFilePath rangeOfString:dirURL.path];
        [relativeFilePath deleteCharactersInRange:range]; // 相対パス部分のみ残す

        OZZipWriteStream *zipWriteStream = [zip writeFileInZipWithName:relativeFilePath
                                                      compressionLevel:OZZipCompressionLevelBest];
        NSData* fileData = [NSData dataWithContentsOfURL:fileURL];
        [zipWriteStream writeData:fileData];
        [zipWriteStream finishedWriting];
    }
    [zip close];
    return;
}

/**
 @brief 対象フォルダ内の全ファイルのURL配列を取得する
 @param aDirectory 検索対象フォルダ
 @return ファイルのURL配列
 @warning pkg及び隠しファイル及びサブフォルダ自体を無視
 */
- (NSArray *)getFileURLListWithFolderURL:(NSURL *)aDirectory {
    NSMutableArray *fileURLList = [NSMutableArray array];
    // フォルダ内のファイルのパスを順番に取得する
    NSFileManager *fileManager = NSFileManager.defaultManager;
    // ディレクトリ用の列挙子を取得する(pkg及び隠しファイルは除外する)
    NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtURL:aDirectory
                                          includingPropertiesForKeys:nil
                                                             options:NSDirectoryEnumerationSkipsPackageDescendants | NSDirectoryEnumerationSkipsHiddenFiles
                                                        errorHandler:nil];
    for (NSURL *subURL in enumerator) {
        BOOL isDir = NO;
        [fileManager fileExistsAtPath:subURL.path isDirectory:&isDir];
        if (isDir) {
            continue;   // フォルダは対象外
        }
        [fileURLList addObject:subURL];
    }
    return fileURLList.copy;
}