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