1 /**
2  * Copyright: Copyright (c) 2010-2011 Jacob Carlborg. All rights reserved.
3  * Authors: Jacob Carlborg
4  * Version: Initial created: Nov 8, 2010
5  * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
6  */
7 module dvm.commands.Fetch;
8 
9 import std.algorithm : splitter, startsWith;
10 import std.exception : assumeUnique;
11 import std.format : format;
12 import std.range : empty, front;
13 import std.regex;
14 import std.stdio : writeln;
15 import std.string : split;
16 
17 import tango.core.Exception;
18 import tango.io.device.File;
19 import tango.io.model.IConduit;
20 import Path = tango.io.Path;
21 import tango.net.InternetAddress;
22 import tango.net.device.Socket;
23 import tango.net.http.HttpGet;
24 import tango.net.http.HttpConst;
25 
26 import dvm.commands.Command;
27 import dvm.util._;
28 import dvm.dvm._;
29 
30 class Fetch : Command
31 {
32     this (string name, string summary = "")
33     {
34         super(name, summary);
35     }
36 
37     this ()
38     {
39         super("fetch", "Fetch a D compiler but don't install it.");
40     }
41 
42     override void execute ()
43     {
44         if (args.any && args.first == "dmc")
45             fetchDMC;
46 
47         else
48         {
49             auto version_ = getDMDVersion();
50             string filename = buildFilename(version_);
51             string url = buildUrl(version_, filename);
52             fetch(url, Path.join(".", filename).assumeUnique);
53         }
54     }
55 
56 protected:
57 
58     enum userAgent = buildUserAgent();
59     enum dmcArchiveName = "dm852c.zip";
60 
61     void fetchDMC (string destinationPath=".")
62     {
63         auto url = "http://ftp.digitalmars.com/Digital_Mars_C++/Patch/" ~ dmcArchiveName;
64         fetch(url, Path.join(destinationPath, dmcArchiveName).assumeUnique);
65     }
66 
67     void fetch (string source, string destination)
68     {
69         if (Path.exists(destination))
70             return;
71 
72         if (options.verbose)
73         {
74             writeln("Fetching:");
75             writeln(options.indentation, "source: ", source);
76             writeln(options.indentation, "destination: ", destination, '\n');
77         }
78 
79         else
80             writeln("Fetching: ", source);
81 
82         createPath(Path.parse(destination).folder);
83         writeFile(downloadFile(source), destination);
84     }
85 
86     void[] downloadFile (string url)
87     {
88         static void print (A...)(A args)
89         {
90             import tango.io.Stdout;
91 
92             static enum string fmt = "{}{}{}{}{}{}{}{}" ~
93                                      "{}{}{}{}{}{}{}{}" ~
94                                      "{}{}{}{}{}{}{}{}";
95 
96             static assert (A.length <= fmt.length / 2, "mambo.io.print :: too many arguments");
97 
98             Stdout.format(fmt[0 .. args.length * 2], args).flush;
99         }
100 
101         auto page = new HttpGet(url);
102         page.setTimeout(30f);
103         page.getRequestHeaders.add(HttpHeader.UserAgent, userAgent);
104         auto buffer = page.open;
105 
106         scope(exit)
107             page.close;
108 
109         checkPageStatus(page, url);
110 
111         // load in chunks in order to display progress
112         int contentLength = page.getResponseHeaders.getInt(HttpHeader.ContentLength);
113 
114         enum width = 40;
115         int num = width;
116 
117         version (Posix)
118         {
119             enum clearLine = "\033[1K"; // clear backwards
120             enum saveCursor = "\0337";
121             enum restoreCursor = "\0338";
122         }
123 
124         else
125         {
126             enum clearLine = "\r";
127 
128             // Leaving these empty string causes a linker error:
129             // http://d.puremagic.com/issues/show_bug.cgi?id=4315
130             enum saveCursor = "\0";
131             enum restoreCursor = "\0";
132         }
133 
134         print(saveCursor);
135 
136         int bytesLeft = contentLength;
137         int chunkSize = bytesLeft / num;
138 
139         while (bytesLeft > 0)
140         {
141             buffer.load(chunkSize > bytesLeft ? bytesLeft : chunkSize);
142             bytesLeft -= chunkSize;
143             int i = 0;
144 
145             print(clearLine ~ restoreCursor ~ saveCursor);
146             print("[");
147 
148             for ( ; i < (width - num); i++)
149                 print("=");
150 
151             print('>');
152 
153             for ( ; i < width; i++)
154                 print(" ");
155 
156             print("]");
157             print(" ", (contentLength - bytesLeft) / 1024, "/", contentLength / 1024, " KB");
158 
159             num--;
160         }
161 
162         writeln(restoreCursor);
163         writeln();
164 
165         return buffer.slice;
166     }
167 
168     void writeFile (void[] data, string filename)
169     {
170         auto file = new File(filename, File.WriteCreate);
171         scope(exit) file.close();
172         file.write(data);
173     }
174 
175     string buildFilename (string ver="")
176     {
177         if (ver == "")
178             ver = getDMDVersion;
179 
180         auto filename = format("dmd.%s.%s.zip", ver, platformForArchive);
181         auto url = buildUrl(ver, filename);
182 
183         return urlExists(url) ? filename : format("dmd.%s.zip", ver);
184     }
185 
186     string buildUrl (string version_, string filename)
187     {
188         auto url = dlangUrl(version_, filename);
189 
190         if (urlExists(url))
191             return url;
192 
193         return digitalMarsUrl(filename);
194     }
195 
196     string digitalMarsUrl (string filename)
197     {
198         return "http://ftp.digitalmars.com/" ~ filename;
199     }
200 
201     string dlangUrl (string version_, string filename)
202     {
203         enum baseUrl = "http://downloads.dlang.org/";
204         string releases = "releases";
205 
206         if (isPreRelease(version_))
207         {
208             releases = "pre-releases";
209             version_ = version_.split("-").front;
210         }
211 
212         return format(baseUrl ~ "%s/%s.x/%s/%s", releases, version_.front, version_, filename);
213     }
214 
215     void createPath (string path)
216     {
217         if (!Path.exists(path))
218             Path.createPath(path);
219     }
220 
221     void checkPageStatus (HttpGet page, string url)
222     {
223         if (page.getStatus == 404)
224             throw new IOException(format(`The resource with URL "%s" could not be found.`, url));
225 
226         else if (!page.isResponseOK)
227             throw new IOException(format(`An unexpected error occurred. The resource "%s" responded with the message "%s" and the status code %s.`, url, page.getResponse.getReason, page.getResponse.getStatus));
228     }
229 
230     bool urlExists (string url, float timeout = 5f)
231     {
232         scope page = new HttpGet(url);
233         page.setTimeout(timeout);
234         page.enableRedirect();
235         page.open();
236 
237         return page.isResponseOK;
238     }
239 
240     string getDMDVersion ()
241     {
242         if (options.latest)
243         {
244             auto vers = getDVersion;
245             return args.first = getLatestDMDVersion(vers);
246         }
247 
248         else
249         {
250             validateArguments();
251             return args.first;
252         }
253     }
254 
255     string getDVersion ()
256     {
257         return args.empty() ? "2" : args.first;
258     }
259 
260     string getLatestDMDVersion (string dVersion)
261     {
262         enum mirrors = [
263             "http://downloads.dlang.org/releases/LATEST",
264             "http://ftp.digitalmars.com/LATEST"
265         ];
266 
267         static string fetchFromMirror(string url)
268         {
269             scope page = new HttpGet(url);
270             page.setTimeout(5f);
271             page.enableRedirect();
272             page.open();
273 
274             return page.isResponseOK ? cast(string) page.read : null;
275         }
276 
277         foreach (mirror ; mirrors)
278         {
279             if (auto content = fetchFromMirror(mirror))
280                 return content;
281         }
282 
283         throw new DvmException("Failed to get the latest DMD version.", __FILE__, __LINE__);
284     }
285 
286     void validateArguments (string errorMessage = null)
287     {
288         if (errorMessage.empty)
289             errorMessage = "Cannot fetch a compiler without specifying a version";
290 
291         if (args.empty)
292             throw new DvmException(errorMessage, __FILE__, __LINE__);
293     }
294 
295     bool isPreRelease (string version_)
296     {
297         immutable parts = version_.split("-");
298 
299         if (parts.length != 2)
300             return false;
301 
302         immutable suffix = parts[1];
303 
304         return suffix.front == 'b' || suffix.startsWith("rc");
305     }
306 
307     string platformForArchive()
308     {
309         auto platform = options.platform;
310 
311         version (FreeBSD)
312             platform ~= options.is64bit ? "-64" : "-32";
313 
314         return platform;
315     }
316 }
317 
318 string buildUserAgent ()
319 {
320     import std.system;
321     import std.string;
322 
323     version (X86) auto architecture = "i386";
324     else version (X86_64) auto architecture = "x86_64";
325     else static assert("Unhandled platform");
326 
327     return format("dvm/%s (%s-%s)", dvm.dvm.Version.Version, architecture, os);
328 }
329 
330 template FetchImpl ()
331 {
332     void execute ()
333     {
334         auto filename = buildFilename;
335         auto url = buildUrl(filename);
336         fetch(url, join(".", filename));
337     }
338 
339     protected void fetch (string source, string destination)
340     {
341         writeFile(downloadFile(source), destination);
342     }
343 
344     private void[] downloadFile (string url)
345     {
346         auto page = new HttpGet(url);
347 
348         if (!page.isResponseOK())
349             throw new IOException(format("%s", page.getResponse.getStatus));
350 
351         return page.read;
352     }
353 
354     private void writeFile (void[] data, string filename)
355     {
356         auto file = new File(filename, File.WriteCreate);
357         file.write(data);
358     }
359 
360     private string buildFilename ()
361     {
362         return "dmd." ~ args.first ~ ".zip";
363     }
364 
365     private string buildUrl (string filename)
366     {
367         return "http://ftp.digitalmars.com/" ~ filename;
368     }
369 }