#
#
# Windows native FTP/HTTP/HTTPS file downloader
# (c) Copyright 2017 Eugene Kabanov
#
# See the file "LICENSE", included in this
# distribution, for details about the copyright.
#
## This module implements native Windows FTP/HTTP/HTTPS downloading feature,
## using ``urlmon.UrlDownloadToFile()``.
##
##
when not (defined(windows) or defined(nimdoc)):
{.error: "Platform is not supported.".}
import os
type
DownloadOptions* = enum
## Available download options
optUseCache, ## Use Windows cache.
optUseProgressCallback, ## Report progress via callback.
optIgnoreSecurity ## Ignore HTTPS security problems.
DownloadStatus* = enum
## Available download status sent to ``progress`` callback.
statusProxyDetecting, ## Automatic Proxy detection.
statusCookieSent ## Cookie will be sent with request.
statusResolving, ## Resolving URL with DNS.
statusConnecting, ## Establish connection to server.
statusRedirecting ## HTTP redirection pending.
statusRequesting, ## Sending request to server.
statusMimetypeAvailable, ## Mimetype received from server.
statusBeginDownloading, ## Download process starting.
statusDownloading, ## Download process pending.
statusEndDownloading, ## Download process finished.
statusCacheAvailable ## File found in Windows cache.
statusUnsupported ## Unsupported status.
statusError ## Error happens.
DownloadProgressCallback* = proc(status: DownloadStatus, progress: uint,
progressMax: uint,
message: string)
## Progress callback.
##
## status
## Indicate current stage of downloading process.
##
## progress
## Number of bytes currently downloaded. Available only, if ``status`` is
## ``statusBeginDownloading``, ``statusDownloading`` or
## ``statusEndDownloading``.
##
## progressMax
## Number of bytes expected to download. Available only, if ``status`` is
## ``statusBeginDownloading``, ``statusDownloading`` or
## ``statusEndDownloading``.
##
## message
## Status message, which depends on ``status`` code.
##
## Available messages' values:
##
## statusResolving
## URL hostname to be resolved.
## statusConnecting
## IP address
## statusMimetypeAvailable
## Downloading resource MIME type.
## statusCacheAvailable
## Path to filename stored in Windows cache.
type
UUID = array[4, uint32]
LONG = clong
ULONG = culong
HRESULT = clong
DWORD = uint32
OLECHAR = uint16
OLESTR = ptr OLECHAR
LPWSTR = OLESTR
UINT = cuint
REFIID = ptr UUID
const
E_NOINTERFACE = 0x80004002'i32
E_NOTIMPL = 0x80004001'i32
S_OK = 0x00000000'i32
CP_UTF8 = 65001'u32
IID_IUnknown = UUID([0'u32, 0'u32, 192'u32, 1174405120'u32])
IID_IBindStatusCallback = UUID([2045430209'u32, 298760953'u32,
2852160140'u32, 195644160'u32])
BINDF_GETNEWESTVERSION = 0x00000010'u32
BINDF_IGNORESECURITYPROBLEM = 0x00000100'u32
BINDF_RESYNCHRONIZE = 0x00000200'u32
BINDF_NO_UI = 0x00000800'u32
BINDF_SILENTOPERATION = 0x00001000'u32
BINDF_PRAGMA_NO_CACHE = 0x00002000'u32
ERROR_FILE_NOT_FOUND = 2
ERROR_ACCESS_DENIED = 5
BINDSTATUS_FINDINGRESOURCE = 1
BINDSTATUS_CONNECTING = 2
BINDSTATUS_REDIRECTING = 3
BINDSTATUS_BEGINDOWNLOADDATA = 4
BINDSTATUS_DOWNLOADINGDATA = 5
BINDSTATUS_ENDDOWNLOADDATA = 6
BINDSTATUS_SENDINGREQUEST = 11
BINDSTATUS_MIMETYPEAVAILABLE = 13
BINDSTATUS_CACHEFILENAMEAVAILABLE = 14
BINDSTATUS_PROXYDETECTING = 32
BINDSTATUS_COOKIE_SENT = 34
type
STGMEDIUM = object
tymed: DWORD
pstg: pointer
pUnkForRelease: pointer
SECURITY_ATTRIBUTES = object
nLength*: uint32
lpSecurityDescriptor*: pointer
bInheritHandle*: int32
BINDINFO = object
cbSize: ULONG
stgmedData: STGMEDIUM
szExtraInfo: LPWSTR
grfBindInfoF: DWORD
dwBindVerb: DWORD
szCustomVerb: LPWSTR
cbstgmedData: DWORD
dwOptions: DWORD
dwOptionsFlags: DWORD
dwCodePage: DWORD
securityAttributes: SECURITY_ATTRIBUTES
iid: UUID
pUnk: pointer
dwReserved: DWORD
IBindStatusCallback = object
vtable: ptr IBindStatusCallbackVTable
options: set[DownloadOptions]
objectRefCount: ULONG
binfoFlags: DWORD
progressCallback: DownloadProgressCallback
PIBindStatusCallback = ptr IBindStatusCallback
LPBINDSTATUSCALLBACK = PIBindStatusCallback
IBindStatusCallbackVTable = object
QueryInterface: proc (self: PIBindStatusCallback,
riid: ptr UUID,
pvObject: ptr pointer): HRESULT {.gcsafe,stdcall.}
AddRef: proc(self: PIBindStatusCallback): ULONG {.gcsafe, stdcall.}
Release: proc(self: PIBindStatusCallback): ULONG {.gcsafe, stdcall.}
OnStartBinding: proc(self: PIBindStatusCallback,
dwReserved: DWORD, pib: pointer): HRESULT
{.gcsafe, stdcall.}
GetPriority: proc(self: PIBindStatusCallback, pnPriority: ptr LONG): HRESULT
{.gcsafe, stdcall.}
OnLowResource: proc(self: PIBindStatusCallback, dwReserved: DWORD): HRESULT
{.gcsafe, stdcall.}
OnProgress: proc(self: PIBindStatusCallback, ulProgress: ULONG,
ulProgressMax: ULONG, ulStatusCode: ULONG,
szStatusText: LPWSTR): HRESULT
{.gcsafe, stdcall.}
OnStopBinding: proc(self: PIBindStatusCallback, hresult: HRESULT,
szError: LPWSTR): HRESULT
{.gcsafe, stdcall.}
GetBindInfo: proc(self: PIBindStatusCallback, grfBINDF: ptr DWORD,
pbindinfo: ptr BINDINFO): HRESULT
{.gcsafe, stdcall.}
OnDataAvailable: proc(self: PIBindStatusCallback, grfBSCF: DWORD,
dwSize: DWORD, pformatetc: pointer,
pstgmed: pointer): HRESULT
{.gcsafe, stdcall.}
OnObjectAvailable: proc(self: PIBindStatusCallback, riid: REFIID,
punk: pointer): HRESULT
{.gcsafe, stdcall.}
template FAILED(hr: HRESULT): bool =
(hr < 0)
proc URLDownloadToFile(pCaller: pointer, szUrl: LPWSTR, szFileName: LPWSTR,
dwReserved: DWORD,
lpfnCb: LPBINDSTATUSCALLBACK): HRESULT
{.stdcall, dynlib: "urlmon.dll", importc: "URLDownloadToFileW".}
proc WideCharToMultiByte(CodePage: UINT, dwFlags: DWORD,
lpWideCharStr: ptr OLECHAR, cchWideChar: cint,
lpMultiByteStr: ptr char, cbMultiByte: cint,
lpDefaultChar: ptr char,
lpUsedDefaultChar: ptr uint32): cint
{.stdcall, dynlib: "kernel32.dll", importc: "WideCharToMultiByte".}
proc MultiByteToWideChar(CodePage: UINT, dwFlags: DWORD,
lpMultiByteStr: ptr char, cbMultiByte: cint,
lpWideCharStr: ptr OLECHAR, cchWideChar: cint): cint
{.stdcall, dynlib: "kernel32.dll", importc: "MultiByteToWideChar".}
proc DeleteUrlCacheEntry(lpszUrlName: LPWSTR): int32
{.stdcall, dynlib: "wininet.dll", importc: "DeleteUrlCacheEntryW".}
proc `==`(a, b: UUID): bool =
result = false
if a[0] == b[0] and a[1] == b[1] and
a[2] == b[2] and a[3] == b[3]:
result = true
proc `$`(bstr: LPWSTR): string =
var buffer: char
var count = WideCharToMultiByte(CP_UTF8, 0, bstr, -1, addr(buffer), 0,
nil, nil)
if count == 0:
raiseOsError(osLastError())
else:
result = newString(count + 8)
let res = WideCharToMultiByte(CP_UTF8, 0, bstr, -1, addr(result[0]), count,
nil, nil)
if res == 0:
raiseOsError(osLastError())
result.setLen(res - 1)
proc toBstring(str: string): LPWSTR =
var buffer: OLECHAR
var count = MultiByteToWideChar(CP_UTF8, 0, unsafeAddr(str[0]), -1,
addr(buffer), 0)
if count == 0:
raiseOsError(osLastError())
else:
result = cast[LPWSTR](alloc0((count + 1) * sizeof(OLECHAR)))
let res = MultiByteToWideChar(CP_UTF8, 0, unsafeAddr(str[0]), -1,
result, count)
if res == 0:
raiseOsError(osLastError())
proc freeBstring(bstr: LPWSTR) =
dealloc(bstr)
proc getStatus(scode: ULONG): DownloadStatus =
case scode
of 0: result = statusError
of BINDSTATUS_PROXYDETECTING: result = statusProxyDetecting
of BINDSTATUS_REDIRECTING: result = statusRedirecting
of BINDSTATUS_COOKIE_SENT: result = statusCookieSent
of BINDSTATUS_FINDINGRESOURCE: result = statusResolving
of BINDSTATUS_CONNECTING: result = statusConnecting
of BINDSTATUS_SENDINGREQUEST: result = statusRequesting
of BINDSTATUS_MIMETYPEAVAILABLE: result = statusMimetypeAvailable
of BINDSTATUS_BEGINDOWNLOADDATA: result = statusBeginDownloading
of BINDSTATUS_DOWNLOADINGDATA: result = statusDownloading
of BINDSTATUS_ENDDOWNLOADDATA: result = statusEndDownloading
of BINDSTATUS_CACHEFILENAMEAVAILABLE: result = statusCacheAvailable
else: result = statusUnsupported
proc addRef(self: PIBindStatusCallback): ULONG {.gcsafe, stdcall.} =
inc(self.objectRefCount)
result = self.objectRefCount
proc release(self: PIBindStatusCallback): ULONG {.gcsafe, stdcall.} =
dec(self.objectRefCount)
result = self.objectRefCount
proc queryInterface(self: PIBindStatusCallback, riid: ptr UUID,
pvObject: ptr pointer): HRESULT {.gcsafe,stdcall.} =
pvObject[] = nil
if riid[] == IID_IUnknown:
pvObject[] = cast[pointer](self)
elif riid[] == IID_IBindStatusCallback:
pvObject[] = cast[pointer](self)
if not isNil(pvObject[]):
discard addRef(self)
result = S_OK
else:
result = E_NOINTERFACE
proc onStartBinding(self: PIBindStatusCallback, dwReserved: DWORD,
pib: pointer): HRESULT {.gcsafe, stdcall.} =
result = S_OK
proc getPriority(self: PIBindStatusCallback,
pnPriority: ptr LONG): HRESULT {.gcsafe, stdcall.} =
result = E_NOTIMPL
proc onLowResource(self: PIBindStatusCallback,
dwReserved: DWORD): HRESULT {.gcsafe, stdcall.} =
result = S_OK
proc onStopBinding(self: PIBindStatusCallback,
hresult: HRESULT, szError: LPWSTR): HRESULT
{.gcsafe, stdcall.} =
result = S_OK
proc getBindInfo(self: PIBindStatusCallback,
grfBINDF: ptr DWORD, pbindinfo: ptr BINDINFO): HRESULT
{.gcsafe, stdcall.} =
var cbSize = pbindinfo.cbSize
zeroMem(cast[pointer](pbindinfo), cbSize)
pbindinfo.cbSize = cbSize
grfBINDF[] = self.binfoFlags
result = S_OK
proc onDataAvailable(self: PIBindStatusCallback,
grfBSCF: DWORD, dwSize: DWORD, pformatetc: pointer,
pstgmed: pointer): HRESULT {.gcsafe, stdcall.} =
result = S_OK
proc onObjectAvailable(self: PIBindStatusCallback,
riid: REFIID, punk: pointer): HRESULT
{.gcsafe, stdcall.} =
result = S_OK
proc onProgress(self: PIBindStatusCallback,
ulProgress: ULONG, ulProgressMax: ULONG, ulStatusCode: ULONG,
szStatusText: LPWSTR): HRESULT {.gcsafe, stdcall.} =
var message: string
if optUseProgressCallback in self.options:
if not isNil(szStatusText):
message = $szStatusText
else:
message = ""
self.progressCallback(getStatus(ulStatusCode), uint(ulProgress),
uint(ulProgressMax), message)
result = S_OK
proc newBindStatusCallback(): IBindStatusCallback =
result = IBindStatusCallback()
result.vtable = cast[ptr IBindStatusCallbackVTable](
alloc0(sizeof(IBindStatusCallbackVTable))
)
result.vtable.QueryInterface = queryInterface
result.vtable.AddRef = addRef
result.vtable.Release = release
result.vtable.OnStartBinding = onStartBinding
result.vtable.GetPriority = getPriority
result.vtable.OnLowResource = onLowResource
result.vtable.OnStopBinding = onStopBinding
result.vtable.GetBindInfo = getBindInfo
result.vtable.OnDataAvailable = onDataAvailable
result.vtable.OnObjectAvailable = onObjectAvailable
result.vtable.OnProgress = onProgress
result.objectRefCount = 1
proc freeBindStatusCallback(v: var IBindStatusCallback) =
dealloc(v.vtable)
proc downloadToFile*(szUrl: string, szFileName: string,
options: set[DownloadOptions] = {},
progresscb: DownloadProgressCallback = nil) =
## Downloads from URL specified in ``szUrl`` to local filesystem path
## specified in ``szFileName``.
##
## szUrl
## URL to download, international names are supported.
## szFileName
## Destination path for downloading resource.
## options
## Downloading options. Currently only 2 options supported.
## progresscb
## Callback procedure, which will be called throughout the download
## process, indicating status and progress.
##
## Available downloading options:
##
## optUseCache
## Try to use Windows cache when downloading.
## optIgnoreSecurity
## Ignore HTTPS security problems, e.g. self-signed HTTPS certificate.
##
var bszUrl = szUrl.toBstring()
var bszFile = szFileName.toBstring()
var bstatus = newBindStatusCallback()
bstatus.options = {}
if optUseCache notin options:
bstatus.options.incl(optUseCache)
let res = DeleteUrlCacheEntry(bszUrl)
if res == 0:
let err = osLastError()
if err.int notin {ERROR_ACCESS_DENIED, ERROR_FILE_NOT_FOUND}:
freeBindStatusCallback(bstatus)
freeBstring(bszUrl)
freeBstring(bszFile)
raiseOsError(err)
bstatus.binfoFlags = BINDF_GETNEWESTVERSION or BINDF_RESYNCHRONIZE or
BINDF_PRAGMA_NO_CACHE or BINDF_NO_UI or
BINDF_SILENTOPERATION
if optIgnoreSecurity in options:
bstatus.binfoFlags = bstatus.binfoFlags or BINDF_IGNORESECURITYPROBLEM
if not isNil(progresscb):
bstatus.options.incl(optUseProgressCallback)
bstatus.progressCallback = progresscb
let res = URLDownloadToFile(nil, bszUrl, bszFile, 0, addr bstatus)
if FAILED(res):
freeBindStatusCallback(bstatus)
freeBstring(bszUrl)
freeBstring(bszFile)
raiseOsError(OSErrorCode(res))
freeBindStatusCallback(bstatus)
freeBstring(bszUrl)
freeBstring(bszFile)
when isMainModule:
proc progress(status: DownloadStatus, progress: uint, progressMax: uint,
message: string) {.procvar,gcsafe.} =
const downset: set[DownloadStatus] = {statusBeginDownloading,
statusDownloading, statusEndDownloading}
if status in downset:
var message = "Downloaded " & $progress & " of " & $progressMax & "\c"
stdout.write(message)
else:
echo "Status [" & $status & "] message = [" & $message & "]"
downloadToFile("https://nim-lang.org/download/mingw64-6.3.0.7z",
"test.zip", {optUseCache}, progress)