PageRenderTime 102ms CodeModel.GetById 28ms app.highlight 64ms RepoModel.GetById 2ms app.codeStats 0ms

/src/luarocks/fs/lua.lua

http://github.com/keplerproject/luarocks
Lua | 1208 lines | 857 code | 125 blank | 226 comment | 215 complexity | 1ff33c7e5b69c7ffa72e5ad132a4068f MD5 | raw file
   1
   2--- Native Lua implementation of filesystem and platform abstractions,
   3-- using LuaFileSystem, LuaSocket, LuaSec, lua-zlib, LuaPosix, MD5.
   4-- module("luarocks.fs.lua")
   5local fs_lua = {}
   6
   7local fs = require("luarocks.fs")
   8
   9local cfg = require("luarocks.core.cfg")
  10local dir = require("luarocks.dir")
  11local util = require("luarocks.util")
  12
  13local socket_ok, zip_ok, lfs_ok, md5_ok, posix_ok, bz2_ok, _
  14local http, ftp, zip, lfs, md5, posix, bz2
  15
  16if cfg.fs_use_modules then
  17   socket_ok, http = pcall(require, "socket.http")
  18   _, ftp = pcall(require, "socket.ftp")
  19   zip_ok, zip = pcall(require, "luarocks.tools.zip")
  20   bz2_ok, bz2 = pcall(require, "bz2")
  21   lfs_ok, lfs = pcall(require, "lfs")
  22   md5_ok, md5 = pcall(require, "md5")
  23   posix_ok, posix = pcall(require, "posix")
  24end
  25
  26local patch = require("luarocks.tools.patch")
  27local tar = require("luarocks.tools.tar")
  28
  29local dir_stack = {}
  30
  31--- Test is file/dir is writable.
  32-- Warning: testing if a file/dir is writable does not guarantee
  33-- that it will remain writable and therefore it is no replacement
  34-- for checking the result of subsequent operations.
  35-- @param file string: filename to test
  36-- @return boolean: true if file exists, false otherwise.
  37function fs_lua.is_writable(file)
  38   assert(file)
  39   file = dir.normalize(file)
  40   local result
  41   if fs.is_dir(file) then
  42      local file2 = dir.path(file, '.tmpluarockstestwritable')
  43      local fh = io.open(file2, 'wb')
  44      result = fh ~= nil
  45      if fh then fh:close() end
  46      os.remove(file2)
  47   else
  48      local fh = io.open(file, 'r+b')
  49      result = fh ~= nil
  50      if fh then fh:close() end
  51   end
  52   return result
  53end
  54
  55local function quote_args(command, ...)
  56   local out = { command }
  57   for _, arg in ipairs({...}) do
  58      assert(type(arg) == "string")
  59      out[#out+1] = fs.Q(arg)
  60   end
  61   return table.concat(out, " ")
  62end
  63
  64--- Run the given command, quoting its arguments.
  65-- The command is executed in the current directory in the dir stack.
  66-- @param command string: The command to be executed. No quoting/escaping
  67-- is applied.
  68-- @param ... Strings containing additional arguments, which are quoted.
  69-- @return boolean: true if command succeeds (status code 0), false
  70-- otherwise.
  71function fs_lua.execute(command, ...)
  72   assert(type(command) == "string")
  73   return fs.execute_string(quote_args(command, ...))
  74end
  75
  76--- Run the given command, quoting its arguments, silencing its output.
  77-- The command is executed in the current directory in the dir stack.
  78-- Silencing is omitted if 'verbose' mode is enabled.
  79-- @param command string: The command to be executed. No quoting/escaping
  80-- is applied.
  81-- @param ... Strings containing additional arguments, which will be quoted.
  82-- @return boolean: true if command succeeds (status code 0), false
  83-- otherwise.
  84function fs_lua.execute_quiet(command, ...)
  85   assert(type(command) == "string")
  86   if cfg.verbose then -- omit silencing output
  87      return fs.execute_string(quote_args(command, ...))
  88   else
  89      return fs.execute_string(fs.quiet(quote_args(command, ...)))
  90   end
  91end
  92
  93function fs.execute_env(env, command, ...)
  94   assert(type(command) == "string")
  95   local envstr = {}
  96   for var, val in pairs(env) do
  97      table.insert(envstr, fs.export_cmd(var, val))
  98   end
  99   return fs.execute_string(table.concat(envstr, "\n") .. "\n" .. quote_args(command, ...))
 100end
 101
 102local tool_available_cache = {}
 103
 104function fs_lua.set_tool_available(tool_name, value)
 105   assert(type(value) == "boolean")
 106   tool_available_cache[tool_name] = value
 107end
 108
 109--- Checks if the given tool is available.
 110-- The tool is executed using a flag, usually just to ask its version.
 111-- @param tool_cmd string: The command to be used to check the tool's presence (e.g. hg in case of Mercurial)
 112-- @param tool_name string: The actual name of the tool (e.g. Mercurial)
 113-- @param arg string: The flag to pass to the tool. '--version' by default.
 114function fs_lua.is_tool_available(tool_cmd, tool_name, arg)
 115   assert(type(tool_cmd) == "string")
 116   assert(type(tool_name) == "string")
 117
 118   arg = arg or "--version"
 119   assert(type(arg) == "string")
 120
 121   local ok
 122   if tool_available_cache[tool_name] ~= nil then
 123      ok = tool_available_cache[tool_name]
 124   else
 125      ok = fs.execute_quiet(tool_cmd, arg)
 126      tool_available_cache[tool_name] = (ok == true)
 127   end
 128
 129   if ok then
 130      return true
 131   else   
 132      local msg = "'%s' program not found. Make sure %s is installed and is available in your PATH " ..
 133                  "(or you may want to edit the 'variables.%s' value in file '%s')"
 134      return nil, msg:format(tool_cmd, tool_name, tool_name:upper(), cfg.config_files.nearest)
 135   end
 136end
 137
 138--- Check the MD5 checksum for a file.
 139-- @param file string: The file to be checked.
 140-- @param md5sum string: The string with the expected MD5 checksum.
 141-- @return boolean: true if the MD5 checksum for 'file' equals 'md5sum', false + msg if not
 142-- or if it could not perform the check for any reason.
 143function fs_lua.check_md5(file, md5sum)
 144   file = dir.normalize(file)
 145   local computed, msg = fs.get_md5(file)
 146   if not computed then
 147      return false, msg
 148   end
 149   if computed:match("^"..md5sum) then
 150      return true
 151   else
 152      return false, "Mismatch MD5 hash for file "..file
 153   end
 154end
 155
 156--- List the contents of a directory.
 157-- @param at string or nil: directory to list (will be the current
 158-- directory if none is given).
 159-- @return table: an array of strings with the filenames representing
 160-- the contents of a directory.
 161function fs_lua.list_dir(at)
 162   local result = {}
 163   for file in fs.dir(at) do
 164      result[#result+1] = file
 165   end
 166   return result
 167end
 168
 169--- Iterate over the contents of a directory.
 170-- @param at string or nil: directory to list (will be the current
 171-- directory if none is given).
 172-- @return function: an iterator function suitable for use with
 173-- the for statement.
 174function fs_lua.dir(at)
 175   if not at then
 176      at = fs.current_dir()
 177   end
 178   at = dir.normalize(at)
 179   if not fs.is_dir(at) then
 180      return function() end
 181   end
 182   return coroutine.wrap(function() fs.dir_iterator(at) end)
 183end
 184
 185--- List the Lua modules at a specific require path.
 186-- eg. `modules("luarocks.cmd")` would return a list of all LuaRocks command
 187-- modules, in the current Lua path.
 188function fs_lua.modules(at)
 189   at = at or ""
 190   if #at > 0 then
 191      -- turn require path into file path
 192      at = at:gsub("%.", package.config:sub(1,1)) .. package.config:sub(1,1)
 193   end
 194
 195   local path = package.path:sub(-1, -1) == ";" and package.path or package.path .. ";"
 196   local paths = {}
 197   for location in path:gmatch("(.-);") do
 198      if location:lower() == "?.lua" then
 199         location = "./?.lua"
 200      end
 201      local _, q_count = location:gsub("%?", "") -- only use the ones with a single '?'
 202      if location:match("%?%.[lL][uU][aA]$") and q_count == 1 then  -- only use when ending with "?.lua"
 203         location = location:gsub("%?%.[lL][uU][aA]$", at)
 204         table.insert(paths, location)
 205      end
 206   end
 207
 208   if #paths == 0 then
 209      return {}
 210   end
 211
 212   local modules = {}
 213   local is_duplicate = {}
 214   for _, path in ipairs(paths) do
 215      local files = fs.list_dir(path)
 216      for _, filename in ipairs(files or {}) do
 217         if filename:match("%.[lL][uU][aA]$") then
 218           filename = filename:sub(1,-5) -- drop the extension
 219           if not is_duplicate[filename] then
 220              is_duplicate[filename] = true
 221              table.insert(modules, filename)
 222           end
 223         end
 224      end
 225   end
 226
 227   return modules
 228end
 229
 230function fs_lua.filter_file(fn, input_filename, output_filename)
 231   local fd, err = io.open(input_filename, "rb")
 232   if not fd then
 233      return nil, err
 234   end
 235
 236   local input, err = fd:read("*a")
 237   fd:close()
 238   if not input then
 239      return nil, err
 240   end
 241
 242   local output, err = fn(input)
 243   if not output then
 244      return nil, err
 245   end
 246
 247   fd, err = io.open(output_filename, "wb")
 248   if not fd then
 249      return nil, err
 250   end
 251
 252   local ok, err = fd:write(output)
 253   fd:close()
 254   if not ok then
 255      return nil, err
 256   end
 257
 258   return true
 259end
 260
 261function fs_lua.system_temp_dir()
 262   return os.getenv("TMPDIR") or os.getenv("TEMP") or "/tmp"
 263end
 264
 265---------------------------------------------------------------------
 266-- LuaFileSystem functions
 267---------------------------------------------------------------------
 268
 269if lfs_ok then
 270
 271--- Run the given command.
 272-- The command is executed in the current directory in the dir stack.
 273-- @param cmd string: No quoting/escaping is applied to the command.
 274-- @return boolean: true if command succeeds (status code 0), false
 275-- otherwise.
 276function fs_lua.execute_string(cmd)
 277   local code = os.execute(cmd)
 278   return (code == 0 or code == true)
 279end
 280
 281--- Obtain current directory.
 282-- Uses the module's internal dir stack.
 283-- @return string: the absolute pathname of the current directory.
 284function fs_lua.current_dir()
 285   return lfs.currentdir()
 286end
 287
 288--- Change the current directory.
 289-- Uses the module's internal dir stack. This does not have exact
 290-- semantics of chdir, as it does not handle errors the same way,
 291-- but works well for our purposes for now.
 292-- @param d string: The directory to switch to.
 293function fs_lua.change_dir(d)
 294   table.insert(dir_stack, lfs.currentdir())
 295   d = dir.normalize(d)
 296   return lfs.chdir(d)
 297end
 298
 299--- Change directory to root.
 300-- Allows leaving a directory (e.g. for deleting it) in
 301-- a crossplatform way.
 302function fs_lua.change_dir_to_root()
 303   local current = lfs.currentdir()
 304   if not current or current == "" then
 305      return false
 306   end
 307   table.insert(dir_stack, current)
 308   lfs.chdir("/") -- works on Windows too
 309   return true
 310end
 311
 312--- Change working directory to the previous in the dir stack.
 313-- @return true if a pop occurred, false if the stack was empty.
 314function fs_lua.pop_dir()
 315   local d = table.remove(dir_stack)
 316   if d then
 317      lfs.chdir(d)
 318      return true
 319   else
 320      return false
 321   end
 322end
 323
 324--- Create a directory if it does not already exist.
 325-- If any of the higher levels in the path name do not exist
 326-- too, they are created as well.
 327-- @param directory string: pathname of directory to create.
 328-- @return boolean or (boolean, string): true on success or (false, error message) on failure.
 329function fs_lua.make_dir(directory)
 330   assert(type(directory) == "string")
 331   directory = dir.normalize(directory)
 332   local path = nil
 333   if directory:sub(2, 2) == ":" then
 334     path = directory:sub(1, 2)
 335     directory = directory:sub(4)
 336   else
 337     if directory:match("^/") then
 338        path = ""
 339     end
 340   end
 341   for d in directory:gmatch("([^/]+)/*") do
 342      path = path and path .. "/" .. d or d
 343      local mode = lfs.attributes(path, "mode")
 344      if not mode then
 345         local ok, err = lfs.mkdir(path)
 346         if not ok then
 347            return false, err
 348         end
 349         ok, err = fs.set_permissions(path, "exec", "all")
 350         if not ok then
 351            return false, err
 352         end
 353      elseif mode ~= "directory" then
 354         return false, path.." is not a directory"
 355      end
 356   end
 357   return true
 358end
 359
 360--- Remove a directory if it is empty.
 361-- Does not return errors (for example, if directory is not empty or
 362-- if already does not exist)
 363-- @param d string: pathname of directory to remove.
 364function fs_lua.remove_dir_if_empty(d)
 365   assert(d)
 366   d = dir.normalize(d)
 367   lfs.rmdir(d)
 368end
 369
 370--- Remove a directory if it is empty.
 371-- Does not return errors (for example, if directory is not empty or
 372-- if already does not exist)
 373-- @param d string: pathname of directory to remove.
 374function fs_lua.remove_dir_tree_if_empty(d)
 375   assert(d)
 376   d = dir.normalize(d)
 377   for i=1,10 do
 378      lfs.rmdir(d)
 379      d = dir.dir_name(d)
 380   end
 381end
 382
 383local function are_the_same_file(f1, f2)
 384   if f1 == f2 then
 385      return true
 386   end
 387   if cfg.is_platform("unix") then
 388      local i1 = lfs.attributes(f1, "ino")
 389      local i2 = lfs.attributes(f2, "ino")
 390      if i1 ~= nil and i1 == i2 then
 391         return true
 392      end
 393   end
 394   return false
 395end
 396
 397--- Copy a file.
 398-- @param src string: Pathname of source
 399-- @param dest string: Pathname of destination
 400-- @param perms string ("read" or "exec") or nil: Permissions for destination
 401-- file or nil to use the source file permissions
 402-- @return boolean or (boolean, string): true on success, false on failure,
 403-- plus an error message.
 404function fs_lua.copy(src, dest, perms)
 405   assert(src and dest)
 406   src = dir.normalize(src)
 407   dest = dir.normalize(dest)
 408   local destmode = lfs.attributes(dest, "mode")
 409   if destmode == "directory" then
 410      dest = dir.path(dest, dir.base_name(src))
 411   end
 412   if are_the_same_file(src, dest) then
 413      return nil, "The source and destination are the same files"
 414   end
 415   local src_h, err = io.open(src, "rb")
 416   if not src_h then return nil, err end
 417   local dest_h, err = io.open(dest, "w+b")
 418   if not dest_h then src_h:close() return nil, err end
 419   while true do
 420      local block = src_h:read(8192)
 421      if not block then break end
 422      dest_h:write(block)
 423   end
 424   src_h:close()
 425   dest_h:close()
 426
 427   local fullattrs
 428   if not perms then
 429      fullattrs = lfs.attributes(src, "permissions")
 430   end
 431   if fullattrs and posix_ok then
 432      return posix.chmod(dest, fullattrs)
 433   else
 434      if not perms then
 435         perms = fullattrs:match("x") and "exec" or "read"
 436      end
 437      return fs.set_permissions(dest, perms, "all")
 438   end
 439end
 440
 441--- Implementation function for recursive copy of directory contents.
 442-- Assumes paths are normalized.
 443-- @param src string: Pathname of source
 444-- @param dest string: Pathname of destination
 445-- @param perms string ("read" or "exec") or nil: Optional permissions.
 446-- If not given, permissions of the source are copied over to the destination.
 447-- @return boolean or (boolean, string): true on success, false on failure
 448local function recursive_copy(src, dest, perms)
 449   local srcmode = lfs.attributes(src, "mode")
 450
 451   if srcmode == "file" then
 452      local ok = fs.copy(src, dest, perms)
 453      if not ok then return false end
 454   elseif srcmode == "directory" then
 455      local subdir = dir.path(dest, dir.base_name(src))
 456      local ok, err = fs.make_dir(subdir)
 457      if not ok then return nil, err end
 458      if pcall(lfs.dir, src) == false then
 459         return false
 460      end
 461      for file in lfs.dir(src) do
 462         if file ~= "." and file ~= ".." then
 463            local ok = recursive_copy(dir.path(src, file), subdir, perms)
 464            if not ok then return false end
 465         end
 466      end
 467   end
 468   return true
 469end
 470
 471--- Recursively copy the contents of a directory.
 472-- @param src string: Pathname of source
 473-- @param dest string: Pathname of destination
 474-- @param perms string ("read" or "exec") or nil: Optional permissions.
 475-- @return boolean or (boolean, string): true on success, false on failure,
 476-- plus an error message.
 477function fs_lua.copy_contents(src, dest, perms)
 478   assert(src and dest)
 479   src = dir.normalize(src)
 480   dest = dir.normalize(dest)
 481   if not fs.is_dir(src) then
 482      return false, src .. " is not a directory"
 483   end
 484   if pcall(lfs.dir, src) == false then
 485      return false, "Permission denied"
 486   end
 487   for file in lfs.dir(src) do
 488      if file ~= "." and file ~= ".." then
 489         local ok = recursive_copy(dir.path(src, file), dest, perms)
 490         if not ok then
 491            return false, "Failed copying "..src.." to "..dest
 492         end
 493      end
 494   end
 495   return true
 496end
 497
 498--- Implementation function for recursive removal of directories.
 499-- Assumes paths are normalized.
 500-- @param name string: Pathname of file
 501-- @return boolean or (boolean, string): true on success,
 502-- or nil and an error message on failure.
 503local function recursive_delete(name)
 504   local ok = os.remove(name)
 505   if ok then return true end
 506   local pok, ok, err = pcall(function()
 507      for file in lfs.dir(name) do
 508         if file ~= "." and file ~= ".." then
 509            local ok, err = recursive_delete(dir.path(name, file))
 510            if not ok then return nil, err end
 511         end
 512      end
 513      local ok, err = lfs.rmdir(name)
 514      return ok, (not ok) and err
 515   end)
 516   if pok then
 517      return ok, err
 518   else
 519      return pok, ok
 520   end
 521end
 522
 523--- Delete a file or a directory and all its contents.
 524-- @param name string: Pathname of source
 525-- @return nil
 526function fs_lua.delete(name)
 527   name = dir.normalize(name)
 528   recursive_delete(name)
 529end
 530
 531--- Internal implementation function for fs.dir.
 532-- Yields a filename on each iteration.
 533-- @param at string: directory to list
 534-- @return nil or (nil and string): an error message on failure 
 535function fs_lua.dir_iterator(at)
 536   local pok, iter, arg = pcall(lfs.dir, at)
 537   if not pok then
 538      return nil, iter
 539   end
 540   for file in iter, arg do
 541      if file ~= "." and file ~= ".." then
 542         coroutine.yield(file)
 543      end
 544   end
 545end
 546
 547--- Implementation function for recursive find.
 548-- Assumes paths are normalized.
 549-- @param cwd string: Current working directory in recursion.
 550-- @param prefix string: Auxiliary prefix string to form pathname.
 551-- @param result table: Array of strings where results are collected.
 552local function recursive_find(cwd, prefix, result)
 553   local pok, iter, arg = pcall(lfs.dir, cwd)
 554   if not pok then
 555      return nil
 556   end
 557   for file in iter, arg do
 558      if file ~= "." and file ~= ".." then
 559         local item = prefix .. file
 560         table.insert(result, item)
 561         local pathname = dir.path(cwd, file)
 562         if lfs.attributes(pathname, "mode") == "directory" then
 563            recursive_find(pathname, item.."/", result)
 564         end
 565      end
 566   end
 567end
 568
 569--- Recursively scan the contents of a directory.
 570-- @param at string or nil: directory to scan (will be the current
 571-- directory if none is given).
 572-- @return table: an array of strings with the filenames representing
 573-- the contents of a directory.
 574function fs_lua.find(at)
 575   assert(type(at) == "string" or not at)
 576   if not at then
 577      at = fs.current_dir()
 578   end
 579   at = dir.normalize(at)
 580   local result = {}
 581   recursive_find(at, "", result)
 582   return result
 583end
 584
 585--- Test for existence of a file.
 586-- @param file string: filename to test
 587-- @return boolean: true if file exists, false otherwise.
 588function fs_lua.exists(file)
 589   assert(file)
 590   file = dir.normalize(file)
 591   return type(lfs.attributes(file)) == "table"
 592end
 593
 594--- Test is pathname is a directory.
 595-- @param file string: pathname to test
 596-- @return boolean: true if it is a directory, false otherwise.
 597function fs_lua.is_dir(file)
 598   assert(file)
 599   file = dir.normalize(file)
 600   return lfs.attributes(file, "mode") == "directory"
 601end
 602
 603--- Test is pathname is a regular file.
 604-- @param file string: pathname to test
 605-- @return boolean: true if it is a file, false otherwise.
 606function fs_lua.is_file(file)
 607   assert(file)
 608   file = dir.normalize(file)
 609   return lfs.attributes(file, "mode") == "file"
 610end
 611
 612-- Set access and modification times for a file.
 613-- @param filename File to set access and modification times for.
 614-- @param time may be a number containing the format returned
 615-- by os.time, or a table ready to be processed via os.time; if
 616-- nil, current time is assumed.
 617function fs_lua.set_time(file, time)
 618   assert(time == nil or type(time) == "table" or type(time) == "number")
 619   file = dir.normalize(file)
 620   if type(time) == "table" then
 621      time = os.time(time)
 622   end
 623   return lfs.touch(file, time)
 624end
 625
 626else -- if not lfs_ok
 627
 628function fs_lua.exists(file)
 629   assert(file)
 630   file = dir.normalize(fs.absolute_name(file))
 631   -- check if file exists by attempting to open it
 632   return util.exists(file)
 633end
 634
 635end
 636
 637---------------------------------------------------------------------
 638-- lua-bz2 functions
 639---------------------------------------------------------------------
 640
 641if bz2_ok then
 642
 643local function bunzip2_string(data)
 644   local decompressor = bz2.initDecompress()
 645   local output, err = decompressor:update(data)
 646   if not output then
 647      return nil, err
 648   end
 649   decompressor:close()
 650   return output
 651end
 652
 653--- Uncompresses a .bz2 file.
 654-- @param infile string: pathname of .bz2 file to be extracted.
 655-- @param outfile string or nil: pathname of output file to be produced.
 656-- If not given, name is derived from input file.
 657-- @return boolean: true on success; nil and error message on failure.
 658function fs_lua.bunzip2(infile, outfile)
 659   assert(type(infile) == "string")
 660   assert(outfile == nil or type(outfile) == "string")
 661   if not outfile then
 662      outfile = infile:gsub("%.bz2$", "")
 663   end
 664
 665   return fs.filter_file(bunzip2_string, infile, outfile)
 666end
 667
 668end
 669
 670---------------------------------------------------------------------
 671-- luarocks.tools.zip functions
 672---------------------------------------------------------------------
 673
 674if zip_ok then
 675
 676function fs_lua.zip(zipfile, ...)
 677   return zip.zip(zipfile, ...)
 678end
 679
 680function fs_lua.unzip(zipfile)
 681   return zip.unzip(zipfile)
 682end
 683
 684function fs_lua.gunzip(infile, outfile)
 685   return zip.gunzip(infile, outfile)
 686end
 687
 688end
 689
 690---------------------------------------------------------------------
 691-- LuaSocket functions
 692---------------------------------------------------------------------
 693
 694if socket_ok then
 695
 696local ltn12 = require("ltn12")
 697local luasec_ok, https = pcall(require, "ssl.https")
 698
 699local redirect_protocols = {
 700   http = http,
 701   https = luasec_ok and https,
 702}
 703
 704local function request(url, method, http, loop_control)
 705   local result = {}
 706   
 707   if cfg.verbose then
 708      print(method, url)
 709   end
 710
 711   local proxy = os.getenv("http_proxy")
 712   if type(proxy) ~= "string" then proxy = nil end
 713   -- LuaSocket's http.request crashes when given URLs missing the scheme part.
 714   if proxy and not proxy:find("://") then
 715      proxy = "http://" .. proxy
 716   end
 717
 718   if cfg.show_downloads then
 719      io.write(method.." "..url.." ...\n")
 720   end
 721   local dots = 0
 722   if cfg.connection_timeout and cfg.connection_timeout > 0 then
 723      http.TIMEOUT = cfg.connection_timeout
 724   end
 725   local res, status, headers, err = http.request {
 726      url = url,
 727      proxy = proxy,
 728      method = method,
 729      redirect = false,
 730      sink = ltn12.sink.table(result),
 731      step = cfg.show_downloads and function(...)
 732         io.write(".")
 733         io.flush()
 734         dots = dots + 1
 735         if dots == 70 then
 736            io.write("\n")
 737            dots = 0
 738         end
 739         return ltn12.pump.step(...)
 740      end,
 741      headers = {
 742         ["user-agent"] = cfg.user_agent.." via LuaSocket"
 743      },
 744   }
 745   if cfg.show_downloads then
 746      io.write("\n")
 747   end
 748   if not res then
 749      return nil, status
 750   elseif status == 301 or status == 302 then
 751      local location = headers.location
 752      if location then
 753         local protocol, rest = dir.split_url(location)
 754         if redirect_protocols[protocol] then
 755            if not loop_control then
 756               loop_control = {}
 757            elseif loop_control[location] then
 758               return nil, "Redirection loop -- broken URL?"
 759            end
 760            loop_control[url] = true
 761            return request(location, method, redirect_protocols[protocol], loop_control)
 762         else
 763            return nil, "URL redirected to unsupported protocol - install luasec to get HTTPS support.", "https"
 764         end
 765      end
 766      return nil, err
 767   elseif status ~= 200 then
 768      return nil, err
 769   else
 770      return result, status, headers, err
 771   end
 772end
 773
 774local function write_timestamp(filename, data)
 775   local fd = io.open(filename, "w")
 776   if fd then
 777      fd:write(data)
 778      fd:close()
 779   end
 780end
 781
 782local function read_timestamp(filename)
 783   local fd = io.open(filename, "r")
 784   if fd then
 785      local data = fd:read("*a")
 786      fd:close()
 787      return data
 788   end
 789end
 790
 791local function fail_with_status(filename, status, headers)
 792   write_timestamp(filename .. ".unixtime", os.time())
 793   write_timestamp(filename .. ".status", status)
 794   return nil, status, headers
 795end
 796
 797-- @param url string: URL to fetch.
 798-- @param filename string: local filename of the file to fetch.
 799-- @param http table: The library to use (http from LuaSocket or LuaSec)
 800-- @param cache boolean: Whether to use a `.timestamp` file to check
 801-- via the HTTP Last-Modified header if the full download is needed.
 802-- @return (boolean | (nil, string, string?)): True if successful, or
 803-- nil, error message and optionally HTTPS error in case of errors.
 804local function http_request(url, filename, http, cache)
 805   if cache then
 806      local status = read_timestamp(filename..".status")
 807      local timestamp = read_timestamp(filename..".timestamp")
 808      if status or timestamp then
 809         local unixtime = read_timestamp(filename..".unixtime")
 810         if tonumber(unixtime) then
 811            local diff = os.time() - tonumber(unixtime)
 812            if status then
 813               if diff < cfg.cache_fail_timeout then
 814                  return nil, status, {}
 815               end
 816            else
 817               if diff < cfg.cache_timeout then
 818                  return true, nil, nil, true
 819               end
 820            end
 821         end
 822
 823         local result, status, headers, err = request(url, "HEAD", http)
 824         if not result then
 825            return fail_with_status(filename, status, headers)
 826         end
 827         if status == 200 and headers["last-modified"] == timestamp then
 828            write_timestamp(filename .. ".unixtime", os.time())
 829            return true, nil, nil, true
 830         end
 831      end
 832   end
 833   local result, status, headers, err = request(url, "GET", http)
 834   if not result then
 835      if status then
 836         return fail_with_status(filename, status, headers)
 837      end
 838   end
 839   if cache and headers["last-modified"] then
 840      write_timestamp(filename .. ".timestamp", headers["last-modified"])
 841      write_timestamp(filename .. ".unixtime", os.time())
 842   end
 843   local file = io.open(filename, "wb")
 844   if not file then return nil, 0, {} end
 845   for _, data in ipairs(result) do
 846      file:write(data)
 847   end
 848   file:close()
 849   return true
 850end
 851
 852local function ftp_request(url, filename)
 853   local content, err = ftp.get(url)
 854   if not content then
 855      return false, err
 856   end
 857   local file = io.open(filename, "wb")
 858   if not file then return false, err end
 859   file:write(content)
 860   file:close()
 861   return true
 862end
 863
 864local downloader_warning = false
 865
 866--- Download a remote file.
 867-- @param url string: URL to be fetched.
 868-- @param filename string or nil: this function attempts to detect the
 869-- resulting local filename of the remote file as the basename of the URL;
 870-- if that is not correct (due to a redirection, for example), the local
 871-- filename can be given explicitly as this second argument.
 872-- @return (boolean, string, boolean):
 873-- In case of success:
 874-- * true
 875-- * a string with the filename
 876-- * true if the file was retrieved from local cache
 877-- In case of failure:
 878-- * false
 879-- * error message
 880function fs_lua.download(url, filename, cache)
 881   assert(type(url) == "string")
 882   assert(type(filename) == "string" or not filename)
 883
 884   filename = fs.absolute_name(filename or dir.base_name(url))
 885
 886   -- delegate to the configured downloader so we don't have to deal with whitelists
 887   if os.getenv("no_proxy") then
 888      return fs.use_downloader(url, filename, cache)
 889   end
 890
 891   local ok, err, https_err, from_cache
 892   if util.starts_with(url, "http:") then
 893      ok, err, https_err, from_cache = http_request(url, filename, http, cache)
 894   elseif util.starts_with(url, "ftp:") then
 895      ok, err = ftp_request(url, filename)
 896   elseif util.starts_with(url, "https:") then
 897      -- skip LuaSec when proxy is enabled since it is not supported
 898      if luasec_ok and not os.getenv("https_proxy") then
 899         local _
 900         ok, err, _, from_cache = http_request(url, filename, https, cache)
 901      else
 902         https_err = true
 903      end
 904   else
 905      err = "Unsupported protocol"
 906   end
 907   if https_err then
 908      local downloader, err = fs.which_tool("downloader")
 909      if not downloader then
 910         return nil, err
 911      end
 912      if not downloader_warning then
 913         util.warning("falling back to "..downloader.." - install luasec to get native HTTPS support")
 914         downloader_warning = true
 915      end
 916      return fs.use_downloader(url, filename, cache)
 917   elseif not ok then
 918      return nil, err
 919   end
 920   return true, filename, from_cache
 921end
 922
 923else --...if socket_ok == false then
 924
 925function fs_lua.download(url, filename, cache)
 926   return fs.use_downloader(url, filename, cache)
 927end
 928
 929end
 930---------------------------------------------------------------------
 931-- MD5 functions
 932---------------------------------------------------------------------
 933
 934if md5_ok then
 935
 936-- Support the interface of lmd5 by lhf in addition to md5 by Roberto
 937-- and the keplerproject.
 938if not md5.sumhexa and md5.digest then
 939   md5.sumhexa = function(msg)
 940      return md5.digest(msg)
 941   end
 942end
 943
 944if md5.sumhexa then
 945
 946--- Get the MD5 checksum for a file.
 947-- @param file string: The file to be computed.
 948-- @return string: The MD5 checksum or nil + error
 949function fs_lua.get_md5(file)
 950   file = fs.absolute_name(file)
 951   local file_handler = io.open(file, "rb")
 952   if not file_handler then return nil, "Failed to open file for reading: "..file end
 953   local computed = md5.sumhexa(file_handler:read("*a"))
 954   file_handler:close()
 955   if computed then return computed end
 956   return nil, "Failed to compute MD5 hash for file "..file
 957end
 958
 959end
 960end
 961
 962---------------------------------------------------------------------
 963-- POSIX functions
 964---------------------------------------------------------------------
 965
 966function fs_lua._unix_rwx_to_number(rwx, neg)
 967   local num = 0
 968   neg = neg or false
 969   for i = 1, 9 do
 970      local c = rwx:sub(10 - i, 10 - i) == "-"
 971      if neg == c then
 972         num = num + 2^(i-1)
 973      end
 974   end
 975   return math.floor(num)
 976end
 977
 978if posix_ok then
 979
 980local octal_to_rwx = {
 981   ["0"] = "---",
 982   ["1"] = "--x",
 983   ["2"] = "-w-",
 984   ["3"] = "-wx",
 985   ["4"] = "r--",
 986   ["5"] = "r-x",
 987   ["6"] = "rw-",
 988   ["7"] = "rwx",
 989}
 990
 991do
 992   local umask_cache
 993   function fs_lua._unix_umask()
 994      if umask_cache then
 995         return umask_cache
 996      end
 997      -- LuaPosix (as of 34.0.4) only returns the umask as rwx
 998      local rwx = posix.umask()
 999      local num = fs_lua._unix_rwx_to_number(rwx, true)
1000      umask_cache = ("%03o"):format(num)
1001      return umask_cache
1002   end
1003end
1004
1005function fs_lua.set_permissions(filename, mode, scope)
1006   local perms
1007   if mode == "read" and scope == "user" then
1008      perms = fs._unix_moderate_permissions("600")
1009   elseif mode == "exec" and scope == "user" then
1010      perms = fs._unix_moderate_permissions("700")
1011   elseif mode == "read" and scope == "all" then
1012      perms = fs._unix_moderate_permissions("644")
1013   elseif mode == "exec" and scope == "all" then
1014      perms = fs._unix_moderate_permissions("755")
1015   else
1016      return false, "Invalid permission " .. mode .. " for " .. scope
1017   end
1018
1019   -- LuaPosix (as of 5.1.15) does not support octal notation...
1020   local new_perms = {}
1021   for c in perms:sub(-3):gmatch(".") do
1022      table.insert(new_perms, octal_to_rwx[c])
1023   end
1024   perms = table.concat(new_perms)
1025   local err = posix.chmod(filename, perms)
1026   return err == 0
1027end
1028
1029function fs_lua.current_user()
1030   return posix.getpwuid(posix.geteuid()).pw_name
1031end
1032
1033-- This call is not available on all systems, see #677
1034if posix.mkdtemp then
1035
1036--- Create a temporary directory.
1037-- @param name_pattern string: name pattern to use for avoiding conflicts
1038-- when creating temporary directory.
1039-- @return string or (nil, string): name of temporary directory or (nil, error message) on failure.
1040function fs_lua.make_temp_dir(name_pattern)
1041   assert(type(name_pattern) == "string")
1042   name_pattern = dir.normalize(name_pattern)
1043
1044   return posix.mkdtemp(fs.system_temp_dir() .. "/luarocks_" .. name_pattern:gsub("/", "_") .. "-XXXXXX")
1045end
1046
1047end -- if posix.mkdtemp
1048
1049end
1050
1051---------------------------------------------------------------------
1052-- Other functions
1053---------------------------------------------------------------------
1054
1055if lfs_ok and not fs_lua.make_temp_dir then
1056
1057function fs_lua.make_temp_dir(name_pattern)
1058   assert(type(name_pattern) == "string")
1059   name_pattern = dir.normalize(name_pattern)
1060
1061   local pattern = fs.system_temp_dir() .. "/luarocks_" .. name_pattern:gsub("/", "_") .. "-"
1062
1063   while true do
1064      local name = pattern .. tostring(math.random(10000000))
1065      if lfs.mkdir(name) then
1066         return name
1067      end
1068   end
1069end
1070
1071end
1072
1073--- Apply a patch.
1074-- @param patchname string: The filename of the patch.
1075-- @param patchdata string or nil: The actual patch as a string.
1076-- @param create_delete boolean: Support creating and deleting files in a patch.
1077function fs_lua.apply_patch(patchname, patchdata, create_delete)
1078   local p, all_ok = patch.read_patch(patchname, patchdata)
1079   if not all_ok then
1080      return nil, "Failed reading patch "..patchname
1081   end
1082   if p then
1083      return patch.apply_patch(p, 1, create_delete)
1084   end
1085end
1086
1087--- Move a file.
1088-- @param src string: Pathname of source
1089-- @param dest string: Pathname of destination
1090-- @param perms string ("read" or "exec") or nil: Permissions for destination
1091-- file or nil to use the source file permissions.
1092-- @return boolean or (boolean, string): true on success, false on failure,
1093-- plus an error message.
1094function fs_lua.move(src, dest, perms)
1095   assert(src and dest)
1096   if fs.exists(dest) and not fs.is_dir(dest) then
1097      return false, "File already exists: "..dest
1098   end
1099   local ok, err = fs.copy(src, dest, perms)
1100   if not ok then
1101      return false, err
1102   end
1103   fs.delete(src)
1104   if fs.exists(src) then
1105      return false, "Failed move: could not delete "..src.." after copy."
1106   end
1107   return true
1108end
1109
1110--- Check if user has write permissions for the command.
1111-- Assumes the configuration variables under cfg have been previously set up.
1112-- @param args table: the args table passed to run() drivers.
1113-- @return boolean or (boolean, string): true on success, false on failure,
1114-- plus an error message.
1115function fs_lua.check_command_permissions(args)
1116   local ok = true
1117   local err = ""
1118   for _, directory in ipairs { cfg.rocks_dir, cfg.deploy_lua_dir, cfg.deploy_bin_dir, cfg.deploy_lua_dir } do
1119      if fs.exists(directory) then
1120         if not fs.is_writable(directory) then
1121            ok = false
1122            err = "Your user does not have write permissions in " .. directory
1123            break
1124         end
1125      else
1126         local root = fs.root_of(directory)
1127         local parent = directory
1128         repeat
1129            parent = dir.dir_name(parent)
1130            if parent == "" then
1131               parent = root
1132            end
1133         until parent == root or fs.exists(parent)
1134         if not fs.is_writable(parent) then
1135            ok = false
1136            err = directory.." does not exist and your user does not have write permissions in " .. parent
1137            break
1138         end
1139      end
1140   end
1141   if ok then
1142      return true
1143   else
1144      if args["local"] or cfg.local_by_default then
1145         err = err .. " \n-- please check your permissions."
1146      else
1147         err = err .. " \n-- you may want to run as a privileged user or use your local tree with --local."
1148      end
1149      return nil, err
1150   end
1151end
1152
1153--- Check whether a file is a Lua script
1154-- When the file can be successfully compiled by the configured
1155-- Lua interpreter, it's considered to be a valid Lua file.
1156-- @param filename filename of file to check
1157-- @return boolean true, if it is a Lua script, false otherwise
1158function fs_lua.is_lua(filename)
1159  filename = filename:gsub([[%\]],"/")   -- normalize on fw slash to prevent escaping issues
1160  local lua = fs.Q(dir.path(cfg.variables["LUA_BINDIR"], cfg.lua_interpreter))  -- get lua interpreter configured
1161  -- execute on configured interpreter, might not be the same as the interpreter LR is run on
1162  local result = fs.execute_string(lua..[[ -e "if loadfile(']]..filename..[[') then os.exit(0) else os.exit(1) end"]])
1163  return (result == true)
1164end
1165
1166--- Unpack an archive.
1167-- Extract the contents of an archive, detecting its format by
1168-- filename extension.
1169-- @param archive string: Filename of archive.
1170-- @return boolean or (boolean, string): true on success, false and an error message on failure.
1171function fs_lua.unpack_archive(archive)
1172   assert(type(archive) == "string")
1173
1174   local ok, err
1175   archive = fs.absolute_name(archive)
1176   if archive:match("%.tar%.gz$") then
1177      local tar_filename = archive:gsub("%.gz$", "")
1178      ok, err = fs.gunzip(archive, tar_filename)
1179      if ok then
1180         ok, err = tar.untar(tar_filename, ".")
1181      end
1182   elseif archive:match("%.tgz$") then
1183      local tar_filename = archive:gsub("%.tgz$", ".tar")
1184      ok, err = fs.gunzip(archive, tar_filename)
1185      if ok then
1186         ok, err = tar.untar(tar_filename, ".")
1187      end
1188   elseif archive:match("%.tar%.bz2$") then
1189      local tar_filename = archive:gsub("%.bz2$", "")
1190      ok, err = fs.bunzip2(archive, tar_filename)
1191      if ok then
1192         ok, err = tar.untar(tar_filename, ".")
1193      end
1194   elseif archive:match("%.zip$") then
1195      ok, err = fs.unzip(archive)
1196   elseif archive:match("%.lua$") or archive:match("%.c$") then
1197      -- Ignore .lua and .c files; they don't need to be extracted.
1198      return true
1199   else
1200      return false, "Couldn't extract archive "..archive..": unrecognized filename extension"
1201   end
1202   if not ok then
1203      return false, "Failed extracting "..archive..": "..err
1204   end
1205   return true
1206end
1207
1208return fs_lua