-
Notifications
You must be signed in to change notification settings - Fork 4
/
unzip.js
201 lines (187 loc) · 6.75 KB
/
unzip.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
var yauzl = require("yauzl");
var path = require("path");
var fs = require("fs");
var util = require("util");
var Transform = require("stream").Transform;
var zipFilePath;
var offsetArg;
var lenArg;
var endArg;
var args = process.argv.slice(2);
for (var i = 0; i < args.length; i++) {
var arg = args[i];
if (arg === "--offset") {
i += 1;
offsetArg = parseInt(args[i]);
if (isNaN(offsetArg)) throw new Error("--offset argument not parsable as an int");
} else if (arg === "--len") {
i += 1;
lenArg = parseInt(args[i]);
if (isNaN(lenArg)) throw new Error("--len argument not parsable as an int");
} else if (arg === "--end") {
i += 1;
endArg = parseInt(args[i]);
if (isNaN(endArg)) throw new Error("--end argument not parsable as an int");
} else if (["-h", "--help"].indexOf(arg) !== -1) {
// print help
zipFilePath = null;
break;
} else if (/^--/.test(arg)) {
throw new Error("unrecognized option: " + arg);
} else {
if (zipFilePath != null) throw new Error("too many arguments");
zipFilePath = arg;
}
}
if (zipFilePath == null || /^-/.test(zipFilePath) || (lenArg != null && endArg != null)) {
console.log(
"usage: node unzip.js [options] path/to/file.zip\n" +
"\n" +
"unzips the specified zip file into the current directory\n" +
"\n" +
"options:\n" +
" --offset START\n" +
" --len LEN\n" +
" --end END\n" +
" interprets the middle of the specified file as a zipfile.\n" +
" starting START number of bytes in from the beginning (default 0).\n" +
" end with length of LEN (default is all the way to the end of the file).\n" +
" or end at byte offset END (exclusive) (default is the end of the file).\n" +
" end can be negative to count backwards from the end of the file\n" +
" (example, `--end -1` excludes the last byte of the file).\n" +
"");
process.exit(1);
}
function mkdirp(dir, cb) {
if (dir === ".") return cb();
fs.stat(dir, function(err) {
if (err == null) return cb(); // already exists
var parent = path.dirname(dir);
mkdirp(parent, function() {
process.stdout.write(dir.replace(/\/$/, "") + "/\n");
fs.mkdir(dir, cb);
});
});
}
if (offsetArg != null || lenArg != null || endArg != null) {
openMiddleOfFile(zipFilePath, {lazyEntries: true}, offsetArg, lenArg, endArg, handleZipFile);
} else {
yauzl.open(zipFilePath, {lazyEntries: true}, handleZipFile);
}
function openMiddleOfFile(zipFilePath, options, offsetArg, lenArg, endArg, handleZipFile) {
fs.open(zipFilePath, "r", function(err, fd) {
if (err != null) throw err;
fs.fstat(fd, function(err, stats) {
// resolve optional parameters
if (offsetArg == null) offsetArg = 0;
if (lenArg == null && endArg == null) endArg = stats.size;
if (endArg == null) endArg = lenArg + offsetArg;
else if (endArg < 0) endArg = stats.size + endArg;
// validate parameters
if (offsetArg < 0) throw new Error("--offset < 0");
if (lenArg < 0) throw new Error("--len < 0");
if (offsetArg > endArg) throw new Error("--offset > --end");
if (endArg > stats.size) throw new Error("--end/--len goes past EOF");
function adjustOffset(n) {
return n + offsetArg;
}
// extend RandomAccessReader
function MiddleOfFileReader() {
yauzl.RandomAccessReader.call(this);
}
util.inherits(MiddleOfFileReader, yauzl.RandomAccessReader);
// implement required and option methods
MiddleOfFileReader.prototype._readStreamForRange = function(start, end) {
return fs.createReadStream(null, {
fd: fd,
// shift the start and end offsets
start: start + offsetArg,
end: end + offsetArg - 1, // the -1 is because fs.createReadStream()'s end option is inclusive
autoClose: false,
});
};
MiddleOfFileReader.prototype.read = function(buffer, offset, length, position, callback) {
// shift the position
fs.read(fd, buffer, offset, length, position + offsetArg, callback);
};
MiddleOfFileReader.prototype.close = function(callback) {
fs.close(fd, callback);
};
yauzl.fromRandomAccessReader(new MiddleOfFileReader(), endArg - offsetArg, options, handleZipFile);
});
});
}
function handleZipFile(err, zipfile) {
if (err) throw err;
// track when we've closed all our file handles
var handleCount = 0;
function incrementHandleCount() {
handleCount++;
}
function decrementHandleCount() {
handleCount--;
if (handleCount === 0) {
console.log("all input and output handles closed");
}
}
incrementHandleCount();
zipfile.on("close", function() {
console.log("closed input file");
decrementHandleCount();
});
zipfile.readEntry();
zipfile.on("entry", function(entry) {
if (/\/$/.test(entry.fileName)) {
// directory file names end with '/'
mkdirp(entry.fileName, function() {
if (err) throw err;
zipfile.readEntry();
});
} else {
// ensure parent directory exists
mkdirp(path.dirname(entry.fileName), function() {
zipfile.openReadStream(entry, function(err, readStream) {
if (err) throw err;
// report progress through large files
var byteCount = 0;
var totalBytes = entry.uncompressedSize;
var lastReportedString = byteCount + "/" + totalBytes + " 0%";
process.stdout.write(entry.fileName + "..." + lastReportedString);
function reportString(msg) {
var clearString = "";
for (var i = 0; i < lastReportedString.length; i++) {
clearString += "\b";
if (i >= msg.length) {
clearString += " \b";
}
}
process.stdout.write(clearString + msg);
lastReportedString = msg;
}
// report progress at 60Hz
var progressInterval = setInterval(function() {
reportString(byteCount + "/" + totalBytes + " " + ((byteCount / totalBytes * 100) | 0) + "%");
}, 1000 / 60);
var filter = new Transform();
filter._transform = function(chunk, encoding, cb) {
byteCount += chunk.length;
cb(null, chunk);
};
filter._flush = function(cb) {
clearInterval(progressInterval);
reportString("");
// delete the "..."
process.stdout.write("\b \b\b \b\b \b\n");
cb();
zipfile.readEntry();
};
// pump file contents
var writeStream = fs.createWriteStream(entry.fileName);
incrementHandleCount();
writeStream.on("close", decrementHandleCount);
readStream.pipe(filter).pipe(writeStream);
});
});
}
});
}