urldownloader.nim 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. #
  2. #
  3. # Windows native FTP/HTTP/HTTPS file downloader
  4. # (c) Copyright 2017 Eugene Kabanov
  5. #
  6. # See the file "LICENSE", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. ## This module implements native Windows FTP/HTTP/HTTPS downloading feature,
  10. ## using ``urlmon.UrlDownloadToFile()``.
  11. ##
  12. ##
  13. when not (defined(windows) or defined(nimdoc)):
  14. {.error: "Platform is not supported.".}
  15. import os
  16. type
  17. DownloadOptions* = enum
  18. ## Available download options
  19. optUseCache, ## Use Windows cache.
  20. optUseProgressCallback, ## Report progress via callback.
  21. optIgnoreSecurity ## Ignore HTTPS security problems.
  22. DownloadStatus* = enum
  23. ## Available download status sent to ``progress`` callback.
  24. statusProxyDetecting, ## Automatic Proxy detection.
  25. statusCookieSent ## Cookie will be sent with request.
  26. statusResolving, ## Resolving URL with DNS.
  27. statusConnecting, ## Establish connection to server.
  28. statusRedirecting ## HTTP redirection pending.
  29. statusRequesting, ## Sending request to server.
  30. statusMimetypeAvailable, ## Mimetype received from server.
  31. statusBeginDownloading, ## Download process starting.
  32. statusDownloading, ## Download process pending.
  33. statusEndDownloading, ## Download process finished.
  34. statusCacheAvailable ## File found in Windows cache.
  35. statusUnsupported ## Unsupported status.
  36. statusError ## Error happens.
  37. DownloadProgressCallback* = proc(status: DownloadStatus, progress: uint,
  38. progressMax: uint,
  39. message: string)
  40. ## Progress callback.
  41. ##
  42. ## status
  43. ## Indicate current stage of downloading process.
  44. ##
  45. ## progress
  46. ## Number of bytes currently downloaded. Available only, if ``status`` is
  47. ## ``statusBeginDownloading``, ``statusDownloading`` or
  48. ## ``statusEndDownloading``.
  49. ##
  50. ## progressMax
  51. ## Number of bytes expected to download. Available only, if ``status`` is
  52. ## ``statusBeginDownloading``, ``statusDownloading`` or
  53. ## ``statusEndDownloading``.
  54. ##
  55. ## message
  56. ## Status message, which depends on ``status`` code.
  57. ##
  58. ## Available messages' values:
  59. ##
  60. ## statusResolving
  61. ## URL hostname to be resolved.
  62. ## statusConnecting
  63. ## IP address
  64. ## statusMimetypeAvailable
  65. ## Downloading resource MIME type.
  66. ## statusCacheAvailable
  67. ## Path to filename stored in Windows cache.
  68. type
  69. UUID = array[4, uint32]
  70. LONG = clong
  71. ULONG = culong
  72. HRESULT = clong
  73. DWORD = uint32
  74. OLECHAR = uint16
  75. OLESTR = ptr OLECHAR
  76. LPWSTR = OLESTR
  77. UINT = cuint
  78. REFIID = ptr UUID
  79. const
  80. E_NOINTERFACE = 0x80004002'i32
  81. E_NOTIMPL = 0x80004001'i32
  82. S_OK = 0x00000000'i32
  83. CP_UTF8 = 65001'u32
  84. IID_IUnknown = UUID([0'u32, 0'u32, 192'u32, 1174405120'u32])
  85. IID_IBindStatusCallback = UUID([2045430209'u32, 298760953'u32,
  86. 2852160140'u32, 195644160'u32])
  87. BINDF_GETNEWESTVERSION = 0x00000010'u32
  88. BINDF_IGNORESECURITYPROBLEM = 0x00000100'u32
  89. BINDF_RESYNCHRONIZE = 0x00000200'u32
  90. BINDF_NO_UI = 0x00000800'u32
  91. BINDF_SILENTOPERATION = 0x00001000'u32
  92. BINDF_PRAGMA_NO_CACHE = 0x00002000'u32
  93. ERROR_FILE_NOT_FOUND = 2
  94. ERROR_ACCESS_DENIED = 5
  95. BINDSTATUS_FINDINGRESOURCE = 1
  96. BINDSTATUS_CONNECTING = 2
  97. BINDSTATUS_REDIRECTING = 3
  98. BINDSTATUS_BEGINDOWNLOADDATA = 4
  99. BINDSTATUS_DOWNLOADINGDATA = 5
  100. BINDSTATUS_ENDDOWNLOADDATA = 6
  101. BINDSTATUS_SENDINGREQUEST = 11
  102. BINDSTATUS_MIMETYPEAVAILABLE = 13
  103. BINDSTATUS_CACHEFILENAMEAVAILABLE = 14
  104. BINDSTATUS_PROXYDETECTING = 32
  105. BINDSTATUS_COOKIE_SENT = 34
  106. type
  107. STGMEDIUM = object
  108. tymed: DWORD
  109. pstg: pointer
  110. pUnkForRelease: pointer
  111. SECURITY_ATTRIBUTES = object
  112. nLength*: uint32
  113. lpSecurityDescriptor*: pointer
  114. bInheritHandle*: int32
  115. BINDINFO = object
  116. cbSize: ULONG
  117. stgmedData: STGMEDIUM
  118. szExtraInfo: LPWSTR
  119. grfBindInfoF: DWORD
  120. dwBindVerb: DWORD
  121. szCustomVerb: LPWSTR
  122. cbstgmedData: DWORD
  123. dwOptions: DWORD
  124. dwOptionsFlags: DWORD
  125. dwCodePage: DWORD
  126. securityAttributes: SECURITY_ATTRIBUTES
  127. iid: UUID
  128. pUnk: pointer
  129. dwReserved: DWORD
  130. IBindStatusCallback = object
  131. vtable: ptr IBindStatusCallbackVTable
  132. options: set[DownloadOptions]
  133. objectRefCount: ULONG
  134. binfoFlags: DWORD
  135. progressCallback: DownloadProgressCallback
  136. PIBindStatusCallback = ptr IBindStatusCallback
  137. LPBINDSTATUSCALLBACK = PIBindStatusCallback
  138. IBindStatusCallbackVTable = object
  139. QueryInterface: proc (self: PIBindStatusCallback,
  140. riid: ptr UUID,
  141. pvObject: ptr pointer): HRESULT {.gcsafe,stdcall.}
  142. AddRef: proc(self: PIBindStatusCallback): ULONG {.gcsafe, stdcall.}
  143. Release: proc(self: PIBindStatusCallback): ULONG {.gcsafe, stdcall.}
  144. OnStartBinding: proc(self: PIBindStatusCallback,
  145. dwReserved: DWORD, pib: pointer): HRESULT
  146. {.gcsafe, stdcall.}
  147. GetPriority: proc(self: PIBindStatusCallback, pnPriority: ptr LONG): HRESULT
  148. {.gcsafe, stdcall.}
  149. OnLowResource: proc(self: PIBindStatusCallback, dwReserved: DWORD): HRESULT
  150. {.gcsafe, stdcall.}
  151. OnProgress: proc(self: PIBindStatusCallback, ulProgress: ULONG,
  152. ulProgressMax: ULONG, ulStatusCode: ULONG,
  153. szStatusText: LPWSTR): HRESULT
  154. {.gcsafe, stdcall.}
  155. OnStopBinding: proc(self: PIBindStatusCallback, hresult: HRESULT,
  156. szError: LPWSTR): HRESULT
  157. {.gcsafe, stdcall.}
  158. GetBindInfo: proc(self: PIBindStatusCallback, grfBINDF: ptr DWORD,
  159. pbindinfo: ptr BINDINFO): HRESULT
  160. {.gcsafe, stdcall.}
  161. OnDataAvailable: proc(self: PIBindStatusCallback, grfBSCF: DWORD,
  162. dwSize: DWORD, pformatetc: pointer,
  163. pstgmed: pointer): HRESULT
  164. {.gcsafe, stdcall.}
  165. OnObjectAvailable: proc(self: PIBindStatusCallback, riid: REFIID,
  166. punk: pointer): HRESULT
  167. {.gcsafe, stdcall.}
  168. template FAILED(hr: HRESULT): bool =
  169. (hr < 0)
  170. proc URLDownloadToFile(pCaller: pointer, szUrl: LPWSTR, szFileName: LPWSTR,
  171. dwReserved: DWORD,
  172. lpfnCb: LPBINDSTATUSCALLBACK): HRESULT
  173. {.stdcall, dynlib: "urlmon.dll", importc: "URLDownloadToFileW".}
  174. proc WideCharToMultiByte(CodePage: UINT, dwFlags: DWORD,
  175. lpWideCharStr: ptr OLECHAR, cchWideChar: cint,
  176. lpMultiByteStr: ptr char, cbMultiByte: cint,
  177. lpDefaultChar: ptr char,
  178. lpUsedDefaultChar: ptr uint32): cint
  179. {.stdcall, dynlib: "kernel32.dll", importc: "WideCharToMultiByte".}
  180. proc MultiByteToWideChar(CodePage: UINT, dwFlags: DWORD,
  181. lpMultiByteStr: ptr char, cbMultiByte: cint,
  182. lpWideCharStr: ptr OLECHAR, cchWideChar: cint): cint
  183. {.stdcall, dynlib: "kernel32.dll", importc: "MultiByteToWideChar".}
  184. proc DeleteUrlCacheEntry(lpszUrlName: LPWSTR): int32
  185. {.stdcall, dynlib: "wininet.dll", importc: "DeleteUrlCacheEntryW".}
  186. proc `==`(a, b: UUID): bool =
  187. result = false
  188. if a[0] == b[0] and a[1] == b[1] and
  189. a[2] == b[2] and a[3] == b[3]:
  190. result = true
  191. proc `$`(bstr: LPWSTR): string =
  192. var buffer: char
  193. var count = WideCharToMultiByte(CP_UTF8, 0, bstr, -1, addr(buffer), 0,
  194. nil, nil)
  195. if count == 0:
  196. raiseOsError(osLastError())
  197. else:
  198. result = newString(count + 8)
  199. let res = WideCharToMultiByte(CP_UTF8, 0, bstr, -1, addr(result[0]), count,
  200. nil, nil)
  201. if res == 0:
  202. raiseOsError(osLastError())
  203. result.setLen(res - 1)
  204. proc toBstring(str: string): LPWSTR =
  205. var buffer: OLECHAR
  206. var count = MultiByteToWideChar(CP_UTF8, 0, unsafeAddr(str[0]), -1,
  207. addr(buffer), 0)
  208. if count == 0:
  209. raiseOsError(osLastError())
  210. else:
  211. result = cast[LPWSTR](alloc0((count + 1) * sizeof(OLECHAR)))
  212. let res = MultiByteToWideChar(CP_UTF8, 0, unsafeAddr(str[0]), -1,
  213. result, count)
  214. if res == 0:
  215. raiseOsError(osLastError())
  216. proc freeBstring(bstr: LPWSTR) =
  217. dealloc(bstr)
  218. proc getStatus(scode: ULONG): DownloadStatus =
  219. case scode
  220. of 0: result = statusError
  221. of BINDSTATUS_PROXYDETECTING: result = statusProxyDetecting
  222. of BINDSTATUS_REDIRECTING: result = statusRedirecting
  223. of BINDSTATUS_COOKIE_SENT: result = statusCookieSent
  224. of BINDSTATUS_FINDINGRESOURCE: result = statusResolving
  225. of BINDSTATUS_CONNECTING: result = statusConnecting
  226. of BINDSTATUS_SENDINGREQUEST: result = statusRequesting
  227. of BINDSTATUS_MIMETYPEAVAILABLE: result = statusMimetypeAvailable
  228. of BINDSTATUS_BEGINDOWNLOADDATA: result = statusBeginDownloading
  229. of BINDSTATUS_DOWNLOADINGDATA: result = statusDownloading
  230. of BINDSTATUS_ENDDOWNLOADDATA: result = statusEndDownloading
  231. of BINDSTATUS_CACHEFILENAMEAVAILABLE: result = statusCacheAvailable
  232. else: result = statusUnsupported
  233. proc addRef(self: PIBindStatusCallback): ULONG {.gcsafe, stdcall.} =
  234. inc(self.objectRefCount)
  235. result = self.objectRefCount
  236. proc release(self: PIBindStatusCallback): ULONG {.gcsafe, stdcall.} =
  237. dec(self.objectRefCount)
  238. result = self.objectRefCount
  239. proc queryInterface(self: PIBindStatusCallback, riid: ptr UUID,
  240. pvObject: ptr pointer): HRESULT {.gcsafe,stdcall.} =
  241. pvObject[] = nil
  242. if riid[] == IID_IUnknown:
  243. pvObject[] = cast[pointer](self)
  244. elif riid[] == IID_IBindStatusCallback:
  245. pvObject[] = cast[pointer](self)
  246. if not isNil(pvObject[]):
  247. discard addRef(self)
  248. result = S_OK
  249. else:
  250. result = E_NOINTERFACE
  251. proc onStartBinding(self: PIBindStatusCallback, dwReserved: DWORD,
  252. pib: pointer): HRESULT {.gcsafe, stdcall.} =
  253. result = S_OK
  254. proc getPriority(self: PIBindStatusCallback,
  255. pnPriority: ptr LONG): HRESULT {.gcsafe, stdcall.} =
  256. result = E_NOTIMPL
  257. proc onLowResource(self: PIBindStatusCallback,
  258. dwReserved: DWORD): HRESULT {.gcsafe, stdcall.} =
  259. result = S_OK
  260. proc onStopBinding(self: PIBindStatusCallback,
  261. hresult: HRESULT, szError: LPWSTR): HRESULT
  262. {.gcsafe, stdcall.} =
  263. result = S_OK
  264. proc getBindInfo(self: PIBindStatusCallback,
  265. grfBINDF: ptr DWORD, pbindinfo: ptr BINDINFO): HRESULT
  266. {.gcsafe, stdcall.} =
  267. var cbSize = pbindinfo.cbSize
  268. zeroMem(cast[pointer](pbindinfo), cbSize)
  269. pbindinfo.cbSize = cbSize
  270. grfBINDF[] = self.binfoFlags
  271. result = S_OK
  272. proc onDataAvailable(self: PIBindStatusCallback,
  273. grfBSCF: DWORD, dwSize: DWORD, pformatetc: pointer,
  274. pstgmed: pointer): HRESULT {.gcsafe, stdcall.} =
  275. result = S_OK
  276. proc onObjectAvailable(self: PIBindStatusCallback,
  277. riid: REFIID, punk: pointer): HRESULT
  278. {.gcsafe, stdcall.} =
  279. result = S_OK
  280. proc onProgress(self: PIBindStatusCallback,
  281. ulProgress: ULONG, ulProgressMax: ULONG, ulStatusCode: ULONG,
  282. szStatusText: LPWSTR): HRESULT {.gcsafe, stdcall.} =
  283. var message: string
  284. if optUseProgressCallback in self.options:
  285. if not isNil(szStatusText):
  286. message = $szStatusText
  287. else:
  288. message = ""
  289. self.progressCallback(getStatus(ulStatusCode), uint(ulProgress),
  290. uint(ulProgressMax), message)
  291. result = S_OK
  292. proc newBindStatusCallback(): IBindStatusCallback =
  293. result = IBindStatusCallback()
  294. result.vtable = cast[ptr IBindStatusCallbackVTable](
  295. alloc0(sizeof(IBindStatusCallbackVTable))
  296. )
  297. result.vtable.QueryInterface = queryInterface
  298. result.vtable.AddRef = addRef
  299. result.vtable.Release = release
  300. result.vtable.OnStartBinding = onStartBinding
  301. result.vtable.GetPriority = getPriority
  302. result.vtable.OnLowResource = onLowResource
  303. result.vtable.OnStopBinding = onStopBinding
  304. result.vtable.GetBindInfo = getBindInfo
  305. result.vtable.OnDataAvailable = onDataAvailable
  306. result.vtable.OnObjectAvailable = onObjectAvailable
  307. result.vtable.OnProgress = onProgress
  308. result.objectRefCount = 1
  309. proc freeBindStatusCallback(v: var IBindStatusCallback) =
  310. dealloc(v.vtable)
  311. proc downloadToFile*(szUrl: string, szFileName: string,
  312. options: set[DownloadOptions] = {},
  313. progresscb: DownloadProgressCallback = nil) =
  314. ## Downloads from URL specified in ``szUrl`` to local filesystem path
  315. ## specified in ``szFileName``.
  316. ##
  317. ## szUrl
  318. ## URL to download, international names are supported.
  319. ## szFileName
  320. ## Destination path for downloading resource.
  321. ## options
  322. ## Downloading options. Currently only 2 options supported.
  323. ## progresscb
  324. ## Callback procedure, which will be called throughout the download
  325. ## process, indicating status and progress.
  326. ##
  327. ## Available downloading options:
  328. ##
  329. ## optUseCache
  330. ## Try to use Windows cache when downloading.
  331. ## optIgnoreSecurity
  332. ## Ignore HTTPS security problems, e.g. self-signed HTTPS certificate.
  333. ##
  334. var bszUrl = szUrl.toBstring()
  335. var bszFile = szFileName.toBstring()
  336. var bstatus = newBindStatusCallback()
  337. bstatus.options = {}
  338. if optUseCache notin options:
  339. bstatus.options.incl(optUseCache)
  340. let res = DeleteUrlCacheEntry(bszUrl)
  341. if res == 0:
  342. let err = osLastError()
  343. if err.int notin {ERROR_ACCESS_DENIED, ERROR_FILE_NOT_FOUND}:
  344. freeBindStatusCallback(bstatus)
  345. freeBstring(bszUrl)
  346. freeBstring(bszFile)
  347. raiseOsError(err)
  348. bstatus.binfoFlags = BINDF_GETNEWESTVERSION or BINDF_RESYNCHRONIZE or
  349. BINDF_PRAGMA_NO_CACHE or BINDF_NO_UI or
  350. BINDF_SILENTOPERATION
  351. if optIgnoreSecurity in options:
  352. bstatus.binfoFlags = bstatus.binfoFlags or BINDF_IGNORESECURITYPROBLEM
  353. if not isNil(progresscb):
  354. bstatus.options.incl(optUseProgressCallback)
  355. bstatus.progressCallback = progresscb
  356. let res = URLDownloadToFile(nil, bszUrl, bszFile, 0, addr bstatus)
  357. if FAILED(res):
  358. freeBindStatusCallback(bstatus)
  359. freeBstring(bszUrl)
  360. freeBstring(bszFile)
  361. raiseOsError(OSErrorCode(res))
  362. freeBindStatusCallback(bstatus)
  363. freeBstring(bszUrl)
  364. freeBstring(bszFile)
  365. when isMainModule:
  366. proc progress(status: DownloadStatus, progress: uint, progressMax: uint,
  367. message: string) {.gcsafe.} =
  368. const downset: set[DownloadStatus] = {statusBeginDownloading,
  369. statusDownloading, statusEndDownloading}
  370. if status in downset:
  371. var message = "Downloaded " & $progress & " of " & $progressMax & "\c"
  372. stdout.write(message)
  373. else:
  374. echo "Status [" & $status & "] message = [" & $message & "]"
  375. downloadToFile("https://nim-lang.org/download/mingw64.7z",
  376. "test.zip", {optUseCache}, progress)