summary refs log blame commit diff stats
path: root/tools/urldownloader.nim
blob: 94f18fa81ae26d6498ad7a6bac6deb6c24b68f44 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
# Copyright (C) 2009, 2010  Roman Zimbelmann <romanz@lavabit.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

if __name__ == '__main__': from __init__ import init; init()

from os.path import realpath, join, dirname

from ranger import fsobject
from ranger.fsobject.file import File
from ranger.fsobject.directory import Directory
from ranger.shared.settings import SettingsAware

SettingsAware._setup()

TESTDIR = realpath(join(dirname(__file__), 'testdir'))
TESTFILE = join(TESTDIR, 'testfile5234148')
NONEXISTANT_DIR = join(TESTDIR, 'nonexistant')

import unittest
class Test1(unittest.TestCase):
	def test_initial_condition(self):
		# Check for the expected initial condition
		dir = Directory(TESTDIR)

		self.assertEqual(dir.path, TESTDIR)
		self.assertFalse(dir.content_loaded)
		self.assertEqual(dir.filenames, None)
		self.assertEqual(dir.files, None)
		self.assertRaises(fsobject.NotLoadedYet, len, dir)

	def test_after_content_loaded(self):
		import os
		# Check whether the directory has the correct list of filenames.
		dir = Directory(TESTDIR)
		dir.load_content()

		self.assertTrue(dir.exists)
		self.assertEqual(type(dir.filenames), list)

		# Get the filenames you expect it to have and sort both before
		# comparing. I don't expect any order after only loading the filenames.
		assumed_filenames = os.listdir(TESTDIR)
		assumed_filenames = list(map(lambda str: os.path.join(TESTDIR, str),
			assumed_filenames))
		assumed_filenames.sort()
		dir.filenames.sort()

		self.assertTrue(len(dir) > 0)
		self.assertEqual(dir.filenames, assumed_filenames)

		# build a file object for each file in the list assumed_filenames
		# and find exactly one equivalent in dir.files
		for name in assumed_filenames:
			f = File(name)
			f.load()
			for dirfile in pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
#
#
#    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)