1 /**
2  * ae.sys.archive
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <vladimir@thecybershadow.net>
12  */
13 
14 module ae.sys.archive;
15 
16 import std.array;
17 import std.datetime;
18 import std.exception;
19 import std.file;
20 import std.path;
21 import std.process;
22 import std.string;
23 
24 import ae.sys.file;
25 import ae.sys.install.sevenzip;
26 import ae.utils.path : haveExecutable;
27 
28 /// Unzips a .zip file to the target directory.
29 void unzip(string zip, string target)
30 {
31 	import std.zip;
32 	auto archive = new ZipArchive(zip.read);
33 	foreach (name, entry; archive.directory)
34 	{
35 		auto path = buildPath(target, name).replace("\\", "/");
36 		ensurePathExists(path);
37 
38 		auto attr = entry.fileAttributes;
39 
40 		if (name.endsWith(`/`))
41 		{
42 			if (!path.exists)
43 				path.mkdirRecurse();
44 		}
45 		else
46 		{
47 			bool isLink = false;
48 			version (Posix)
49 			{
50 				import core.sys.posix.sys.stat;
51 				if (S_ISLNK(attr))
52 					isLink = true;
53 			}
54 			if (isLink)
55 			{
56 				symlink(cast(string)archive.expand(entry), path);
57 				continue; // Don't try to chmod the link target!
58 			}
59 			else
60 				std.file.write(path, archive.expand(entry));
61 		}
62 
63 		if (attr)
64 			path.setAttributes(attr);
65 
66 		auto time = entry.time().DosFileTimeToSysTime(UTC());
67 		path.setTimes(time, time);
68 	}
69 }
70 
71 /// Unpacks a file with 7-Zip to the specified directory,
72 /// installing it locally if necessary.
73 void un7z(string archive, string target)
74 {
75 	sevenZip.require();
76 	target.mkdirRecurse();
77 	auto pid = spawnProcess([sevenZip.exe, "x", "-o" ~ target, archive]);
78 	enforce(pid.wait() == 0, "Extraction failed");
79 }
80 
81 /// Unpacks an archive to the specified directory.
82 /// Uses std.zip for .zip files, and invokes tar (if available)
83 /// or 7-Zip (installing it locally if necessary) for other file types.
84 /// Always unpacks compressed tar archives in one go.
85 void unpack(string archive, string target)
86 {
87 	bool untar(string longExtension, string shortExtension, string tarSwitch, string unpacker)
88 	{
89 		if ((archive.toLower().endsWith(longExtension) || archive.toLower().endsWith(shortExtension)) && haveExecutable(unpacker))
90 		{
91 			target.mkdirRecurse();
92 			auto pid = spawnProcess(["tar", "xf", archive, tarSwitch, "--directory", target]);
93 			enforce(pid.wait() == 0, "Extraction failed");
94 			return true;
95 		}
96 		return false;
97 	}
98 
99 	if (archive.toLower().endsWith(".zip"))
100 		archive.unzip(target);
101 	else
102 	if (haveExecutable("tar") && (
103 		untar(".tar.gz", ".tgz", "--gzip", "gzip") ||
104 		untar(".tar.bz2", ".tbz", "--bzip2", "bzip2") ||
105 		untar(".tar.lzma", ".tlz", "--lzma", "lzma") ||
106 		untar(".tar.xz", ".txz", "--xz", "xz")))
107 		{}
108 	else
109 	if (archive.extension.toLower == ".rar" && haveExecutable("unrar"))
110 	{
111 		target.mkdirRecurse();
112 		auto pid = spawnProcess(["unrar", "x", archive, target]);
113 		enforce(pid.wait() == 0, "Extraction failed");
114 	}
115 	else
116 	{
117 		auto tar = archive.stripExtension;
118 		if (tar.extension.toLower == ".tar")
119 		{
120 			un7z(archive, archive.dirName);
121 			enforce(tar.exists, "Expected to unpack " ~ archive ~ " to " ~ tar);
122 			scope(exit) tar.remove();
123 			un7z(tar, target);
124 		}
125 		else
126 			un7z(archive, target);
127 	}
128 }