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

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

# 期待される出力結果
- 下記ファイル構造を保ったままzip化を行う。
- 下記フォルダをダウンロードフォルダ直下に配置

![image.png](https://qiita-image-store.s3.amazonaws.com/0/198225/e5e5038c-04a7-0a74-90e9-e56d5545e453.png)

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

![image.png](https://qiita-image-store.s3.amazonaws.com/0/198225/c882d89b-ac43-09c9-282f-dad593114cb2.png)

# 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ファイルを作成すると解凍でエラーが発生する。（下記コードで行うとエラーが発生する）

``` ObjectiveC
    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];
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/198225/f4ab14b7-da9a-41d1-ca9d-0816a7387706.png)

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

https://stackoverflow.com/questions/12584530/objective-zip-cannot-open-zip

>  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モードを使用すると良いそうです。

``` ObjectiveC
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"`を指定する。

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

# 作成したプログラム
## 冒頭部
``` ObjectiveC
#import "AppDelegate.h"
#import "Objective-Zip.h"
static NSString *const kTargetFolderPath = @"~/Downloads/SampleFolder"; // zip化対象フォルダ
static NSString *const kZipName          = @"result.zip";               // zip化する際のファイル名
```

## 呼び出し部
``` ObjectiveC
NSString *targetFolderPath = [kTargetFolderPath stringByExpandingTildeInPath];
[self zipFilesWithFolderURL:[NSURL fileURLWithPath:targetFolderPath] zipName:kZipName];
```
## 関数部
``` ObjectiveC
/**
 @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;
}
```