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