Skip to content

Commit

Permalink
Add ZIP support
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelmeuli committed Apr 21, 2020
1 parent a790988 commit 9800201
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 40 deletions.
16 changes: 16 additions & 0 deletions Glance.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@
7EB36480244B03CC00D7F96F /* shared-chroma.css in Resources */ = {isa = PBXBuildFile; fileRef = 7EB3643A244B03CC00D7F96F /* shared-chroma.css */; };
7EB36482244B03CC00D7F96F /* markdown-main.css in Resources */ = {isa = PBXBuildFile; fileRef = 7EB3643D244B03CC00D7F96F /* markdown-main.css */; };
7EB36484244B0CCE00D7F96F /* TablePreviewVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EB36483244B0CCE00D7F96F /* TablePreviewVC.swift */; };
7EB36488244B65D900D7F96F /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EB36487244B65D900D7F96F /* String.swift */; };
7EB3648A244B665700D7F96F /* ZIPPreviewVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EB36489244B665700D7F96F /* ZIPPreviewVC.swift */; };
7EB3648C244B6C8B00D7F96F /* OutlinePreviewView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7EB3648B244B6C8B00D7F96F /* OutlinePreviewView.xib */; };
7EB3648E244B6DC100D7F96F /* OutlinePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EB3648D244B6DC100D7F96F /* OutlinePreviewView.swift */; };
7EB36490244B6EF600D7F96F /* OutlinePreviewVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EB3648F244B6EF600D7F96F /* OutlinePreviewVC.swift */; };
Expand Down Expand Up @@ -257,6 +259,8 @@
7EB3643A244B03CC00D7F96F /* shared-chroma.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = "shared-chroma.css"; sourceTree = "<group>"; };
7EB3643D244B03CC00D7F96F /* markdown-main.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = "markdown-main.css"; sourceTree = "<group>"; };
7EB36483244B0CCE00D7F96F /* TablePreviewVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TablePreviewVC.swift; sourceTree = "<group>"; };
7EB36487244B65D900D7F96F /* String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
7EB36489244B665700D7F96F /* ZIPPreviewVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZIPPreviewVC.swift; sourceTree = "<group>"; };
7EB3648B244B6C8B00D7F96F /* OutlinePreviewView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OutlinePreviewView.xib; sourceTree = "<group>"; };
7EB3648D244B6DC100D7F96F /* OutlinePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutlinePreviewView.swift; sourceTree = "<group>"; };
7EB3648F244B6EF600D7F96F /* OutlinePreviewVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutlinePreviewVC.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -332,6 +336,7 @@
7E413F7F2418DD6200CFBB1D /* CSVPreviewVC.swift */,
7EB7491824228549007265A4 /* JupyterPreviewVC.swift */,
7E1DC520240E6D8000D0A061 /* MarkdownPreviewVC.swift */,
7EB36489244B665700D7F96F /* ZIPPreviewVC.swift */,
);
path = FileTypes;
sourceTree = "<group>";
Expand Down Expand Up @@ -412,6 +417,7 @@
7E6EF1FE240CC802009E4199 /* QLPlugin */ = {
isa = PBXGroup;
children = (
7EB36486244B65D900D7F96F /* Extensions */,
7EB36485244B65BC00D7F96F /* Protocols */,
7E1DC51D240E68AF00D0A061 /* Views */,
7E1DC51C240E674300D0A061 /* Utils */,
Expand Down Expand Up @@ -556,6 +562,14 @@
path = Protocols;
sourceTree = "<group>";
};
7EB36486244B65D900D7F96F /* Extensions */ = {
isa = PBXGroup;
children = (
7EB36487244B65D900D7F96F /* String.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
7ECC8CE5240CB4CC000D6970 = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -776,6 +790,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
7EB36488244B65D900D7F96F /* String.swift in Sources */,
7EAC01EC240D220B009505D0 /* File.swift in Sources */,
7EB36490244B6EF600D7F96F /* OutlinePreviewVC.swift in Sources */,
7EB3648E244B6DC100D7F96F /* OutlinePreviewView.swift in Sources */,
Expand All @@ -796,6 +811,7 @@
7E413F802418DD6200CFBB1D /* CSVPreviewVC.swift in Sources */,
7E1DC52A240E6FDE00D0A061 /* CodePreviewVC.swift in Sources */,
7E9F0D7E24168870007F1008 /* Script.swift in Sources */,
7EB3648A244B665700D7F96F /* ZIPPreviewVC.swift in Sources */,
7E9F0D7D24168870007F1008 /* Stylesheet.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
38 changes: 24 additions & 14 deletions Glance/Credits.rtf
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica-Bold;\f1\fswiss\fcharset0 Helvetica;}
{\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;}
{\*\listtable{\list\listtemplateid1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid1\'01\uc0\u8226 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid1}}
{\*\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}}
{\*\listtable{\list\listtemplateid1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid1\'01\uc0\u8226 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid1}
{\list\listtemplateid2\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid101\'01\uc0\u8226 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid2}
{\list\listtemplateid3\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid201\'01\uc0\u8226 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid3}}
{\*\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}{\listoverride\listid2\listoverridecount0\ls2}{\listoverride\listid3\listoverridecount0\ls3}}
\paperw11900\paperh16840\margl1440\margr1440\vieww10800\viewh8400\viewkind0
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\qc\partightenfactor0

Expand All @@ -14,22 +16,30 @@
\f1\b0\fs22 \
\pard\tx220\tx720\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\li720\fi-720\pardirnatural\qc\partightenfactor0
\ls1\ilvl0
\f0\b \cf0 Source code
\f1\b0 \
.js, .json, .swift, .yml, \'85\
\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\pardirnatural\qc\partightenfactor0
\cf0 \
\pard\tx220\tx720\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\li720\fi-720\pardirnatural\qc\partightenfactor0
\ls2\ilvl0
\f0\b \cf0 Markdown
\f1\b0 \
.md, .markdown, .mdown, .mkdn, .mkd\
\
\ls2\ilvl0
\f0\b Archives\
\ls2\ilvl0
\f1\b0 .zip\
\
\pard\tx220\tx720\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\li720\fi-720\pardirnatural\qc\partightenfactor0
\ls3\ilvl0
\f0\b \cf0 CSV/TSV
\f1\b0 \
.csv, .tab, .tsv\
\
\ls1\ilvl0
\ls3\ilvl0
\f0\b Jupyter Notebook\
\ls1\ilvl0
\ls3\ilvl0
\f1\b0 .ipynb\
\
\ls1\ilvl0
\f0\b Markdown
\f1\b0 \
.md, .markdown, .mdown, .mkdn, .mkd\
\
\ls1\ilvl0
\f0\b Source code
\f1\b0 \
.js, .json, .swift, .yml, \'85\
}
26 changes: 26 additions & 0 deletions QLPlugin/Extensions/String.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Foundation

extension String {
/// Returns all matches and capturing groups for the provided regular expression applied to the
/// string
///
/// Source: https://stackoverflow.com/a/40040472/6767508
func matchRegex(regex: String) -> [[String]] {
guard let regex = try? NSRegularExpression(pattern: regex, options: []) else {
return []
}
let nsString = self as NSString
let results = regex.matches(
in: self,
options: [],
range: NSRange(location: 0, length: nsString.length)
)
return results.map { result in
(0 ..< result.numberOfRanges).map {
result.range(at: $0).location != NSNotFound
? nsString.substring(with: result.range(at: $0))
: ""
}
}
}
}
3 changes: 3 additions & 0 deletions QLPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
<!-- Base UTI -->
<string>public.data</string>

<!-- Archive -->
<string>public.zip-archive</string> <!-- .zip -->

<!-- CSV -->
<string>dyn.ah62d4rv4ge81k2pc</string> <!-- .tab -->
<string>public.comma-separated-values-text</string> <!-- .csv -->
Expand Down
2 changes: 1 addition & 1 deletion QLPlugin/MainVC.xib
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customView translatesAutoresizingMaskIntoConstraints="NO" id="c22-O7-iKe" userLabel="Preview View">
<rect key="frame" x="0.0" y="0.0" width="480" height="272"/>
<rect key="frame" x="0.0" y="0.0" width="800" height="500"/>
<point key="canvasLocation" x="139" y="154"/>
</customView>
</objects>
Expand Down
100 changes: 100 additions & 0 deletions QLPlugin/Views/FileTypes/ZipPreviewVC.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import Cocoa
import Foundation
import os.log
import SwiftExec

struct ZIPInfo {
/// Percentage by which the ZIP contents have been compressed
let compressionFactor: Double
/// Compressed content size in bytes
let sizeCompressed: Int
/// Uncompressed content size in bytes
let sizeUncompressed: Int
/// Files in the ZIP archive
let fileTree: FileTree
}

class ZIPPreviewVC: OutlinePreviewVC, PreviewVC {
let secondLineRegex = #"Zip file size: (\d+) bytes, number of entries: \d+"#
let contentLinesRegex =
#"([\w-]{10}) .* \w+ +(\d+) \w+ \w+ (\d{2}-\w{3}-\d{2} \d{2}:\d{2}) (.*)"#
let lastLineRegex = #"^\d+ files, (\d+) bytes uncompressed, \d+ bytes compressed: +([\d.]+)%$"#

let byteCountFormatter = ByteCountFormatter()
let dateFormatter = DateFormatter()

override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?, file: File) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil, file: file)
dateFormatter.dateFormat = "yy-MMM-dd HH:mm" // Date format used in `zipinfo` output
}

/// Parses the output of the `zipinfo` command
private func parseZIPInfo(lines: [String.SubSequence]) -> ZIPInfo {
var compressionFactor: Double = 0
var sizeCompressed: Int = 0
var sizeUncompressed: Int = 0
let fileTree = FileTree()

// First line: "Archive: my-zip.zip" -> Skip

// Second line: "Zip file size: 99791 bytes, number of entries: 152"
let secondLineMatched = String(lines[1]).matchRegex(regex: secondLineRegex)
sizeCompressed = Int(secondLineMatched[0][1]) ?? -1

// Content lines: "drwxr-xr-x 2.0 unx 0 bx stor 20-Jan-13 19:38 my-zip/dir/"
// - "-" as first character indicates a file, "d" a directory
// - "0 bx" indicates the number of bytes
let contentLinesString = lines[2 ... lines.count - 2].joined(separator: "\n")
let contentLinesMatched = contentLinesString.matchRegex(regex: contentLinesRegex)
for match in contentLinesMatched {
// Ignore "__MACOSX" subdirectory (ZIP resource fork created by macOS)
if !match[3].hasPrefix("__MACOSX/") {
do {
// Add file/directory node to tree
try fileTree.addNode(
path: match[4],
isDirectory: match[1].first == "d",
size: Int(match[2]) ?? -1,
dateModified: dateFormatter.date(from: match[3]) ?? Date()
)
} catch {
os_log("%s", type: .error, error.localizedDescription)
}
}
}

// Last line: "152 files, 192919 bytes uncompressed, 65061 bytes compressed: 66.3%"
let lastLineMatched = String(lines.last ?? "").matchRegex(regex: lastLineRegex)
sizeUncompressed = Int(lastLineMatched[0][1]) ?? -1
compressionFactor = Double(lastLineMatched[0][2]) ?? -1

return ZIPInfo(
compressionFactor: compressionFactor,
sizeCompressed: sizeCompressed,
sizeUncompressed: sizeUncompressed,
fileTree: fileTree
)
}

func loadPreview() throws {
// Run `zipinfo` command
let result = try exec(
program: "/usr/bin/zipinfo",
arguments: [file.path]
)

// Parse command output
let stdoutLines = (result.stdout ?? "").split(separator: "\n")
let zipInfo = parseZIPInfo(lines: stdoutLines)

// Load data into outline view
loadData(
fileTree: zipInfo.fileTree,
labelText: """
Size uncompressed: \(byteCountFormatter.string(for: zipInfo.sizeUncompressed) ?? "--")
Size compressed: \(byteCountFormatter.string(for: zipInfo.sizeCompressed) ?? "--")
Compression factor: \(zipInfo.compressionFactor)
"""
)
}
}
2 changes: 2 additions & 0 deletions QLPlugin/Views/PreviewVCFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class PreviewVCFactory {
return MarkdownPreviewVC.self
case "ipynb":
return JupyterPreviewVC.self
case "zip":
return ZIPPreviewVC.self
default:
return CodePreviewVC.self
}
Expand Down
Loading

0 comments on commit 9800201

Please sign in to comment.