osfiles.nim 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. include system/inclrtl
  2. import std/private/since
  3. import std/oserrors
  4. import oscommon
  5. export fileExists
  6. import ospaths2, ossymlinks
  7. ## .. importdoc:: osdirs.nim, os.nim
  8. when defined(nimPreviewSlimSystem):
  9. import std/[syncio, assertions, widestrs]
  10. when weirdTarget:
  11. discard
  12. elif defined(windows):
  13. import std/winlean
  14. elif defined(posix):
  15. import std/[posix, times]
  16. proc toTime(ts: Timespec): times.Time {.inline.} =
  17. result = initTime(ts.tv_sec.int64, ts.tv_nsec.int)
  18. else:
  19. {.error: "OS module not ported to your operating system!".}
  20. when weirdTarget:
  21. {.pragma: noWeirdTarget, error: "this proc is not available on the NimScript/js target".}
  22. else:
  23. {.pragma: noWeirdTarget.}
  24. when defined(nimscript):
  25. # for procs already defined in scriptconfig.nim
  26. template noNimJs(body): untyped = discard
  27. elif defined(js):
  28. {.pragma: noNimJs, error: "this proc is not available on the js target".}
  29. else:
  30. {.pragma: noNimJs.}
  31. type
  32. FilePermission* = enum ## File access permission, modelled after UNIX.
  33. ##
  34. ## See also:
  35. ## * `getFilePermissions`_
  36. ## * `setFilePermissions`_
  37. ## * `FileInfo object`_
  38. fpUserExec, ## execute access for the file owner
  39. fpUserWrite, ## write access for the file owner
  40. fpUserRead, ## read access for the file owner
  41. fpGroupExec, ## execute access for the group
  42. fpGroupWrite, ## write access for the group
  43. fpGroupRead, ## read access for the group
  44. fpOthersExec, ## execute access for others
  45. fpOthersWrite, ## write access for others
  46. fpOthersRead ## read access for others
  47. proc getFilePermissions*(filename: string): set[FilePermission] {.
  48. rtl, extern: "nos$1", tags: [ReadDirEffect], noWeirdTarget.} =
  49. ## Retrieves file permissions for `filename`.
  50. ##
  51. ## `OSError` is raised in case of an error.
  52. ## On Windows, only the ``readonly`` flag is checked, every other
  53. ## permission is available in any case.
  54. ##
  55. ## See also:
  56. ## * `setFilePermissions proc`_
  57. ## * `FilePermission enum`_
  58. when defined(posix):
  59. var a: Stat
  60. if stat(filename, a) < 0'i32: raiseOSError(osLastError(), filename)
  61. result = {}
  62. if (a.st_mode and S_IRUSR.Mode) != 0.Mode: result.incl(fpUserRead)
  63. if (a.st_mode and S_IWUSR.Mode) != 0.Mode: result.incl(fpUserWrite)
  64. if (a.st_mode and S_IXUSR.Mode) != 0.Mode: result.incl(fpUserExec)
  65. if (a.st_mode and S_IRGRP.Mode) != 0.Mode: result.incl(fpGroupRead)
  66. if (a.st_mode and S_IWGRP.Mode) != 0.Mode: result.incl(fpGroupWrite)
  67. if (a.st_mode and S_IXGRP.Mode) != 0.Mode: result.incl(fpGroupExec)
  68. if (a.st_mode and S_IROTH.Mode) != 0.Mode: result.incl(fpOthersRead)
  69. if (a.st_mode and S_IWOTH.Mode) != 0.Mode: result.incl(fpOthersWrite)
  70. if (a.st_mode and S_IXOTH.Mode) != 0.Mode: result.incl(fpOthersExec)
  71. else:
  72. wrapUnary(res, getFileAttributesW, filename)
  73. if res == -1'i32: raiseOSError(osLastError(), filename)
  74. if (res and FILE_ATTRIBUTE_READONLY) != 0'i32:
  75. result = {fpUserExec, fpUserRead, fpGroupExec, fpGroupRead,
  76. fpOthersExec, fpOthersRead}
  77. else:
  78. result = {fpUserExec..fpOthersRead}
  79. proc setFilePermissions*(filename: string, permissions: set[FilePermission],
  80. followSymlinks = true)
  81. {.rtl, extern: "nos$1", tags: [ReadDirEffect, WriteDirEffect],
  82. noWeirdTarget.} =
  83. ## Sets the file permissions for `filename`.
  84. ##
  85. ## If `followSymlinks` set to true (default) and ``filename`` points to a
  86. ## symlink, permissions are set to the file symlink points to.
  87. ## `followSymlinks` set to false is a noop on Windows and some POSIX
  88. ## systems (including Linux) on which `lchmod` is either unavailable or always
  89. ## fails, given that symlinks permissions there are not observed.
  90. ##
  91. ## `OSError` is raised in case of an error.
  92. ## On Windows, only the ``readonly`` flag is changed, depending on
  93. ## ``fpUserWrite`` permission.
  94. ##
  95. ## See also:
  96. ## * `getFilePermissions proc`_
  97. ## * `FilePermission enum`_
  98. when defined(posix):
  99. var p = 0.Mode
  100. if fpUserRead in permissions: p = p or S_IRUSR.Mode
  101. if fpUserWrite in permissions: p = p or S_IWUSR.Mode
  102. if fpUserExec in permissions: p = p or S_IXUSR.Mode
  103. if fpGroupRead in permissions: p = p or S_IRGRP.Mode
  104. if fpGroupWrite in permissions: p = p or S_IWGRP.Mode
  105. if fpGroupExec in permissions: p = p or S_IXGRP.Mode
  106. if fpOthersRead in permissions: p = p or S_IROTH.Mode
  107. if fpOthersWrite in permissions: p = p or S_IWOTH.Mode
  108. if fpOthersExec in permissions: p = p or S_IXOTH.Mode
  109. if not followSymlinks and filename.symlinkExists:
  110. when declared(lchmod):
  111. if lchmod(filename, cast[Mode](p)) != 0:
  112. raiseOSError(osLastError(), $(filename, permissions))
  113. else:
  114. if chmod(filename, cast[Mode](p)) != 0:
  115. raiseOSError(osLastError(), $(filename, permissions))
  116. else:
  117. wrapUnary(res, getFileAttributesW, filename)
  118. if res == -1'i32: raiseOSError(osLastError(), filename)
  119. if fpUserWrite in permissions:
  120. res = res and not FILE_ATTRIBUTE_READONLY
  121. else:
  122. res = res or FILE_ATTRIBUTE_READONLY
  123. wrapBinary(res2, setFileAttributesW, filename, res)
  124. if res2 == - 1'i32: raiseOSError(osLastError(), $(filename, permissions))
  125. const hasCCopyfile = defined(osx) and not defined(nimLegacyCopyFile)
  126. # xxx instead of `nimLegacyCopyFile`, support something like: `when osxVersion >= (10, 5)`
  127. when hasCCopyfile:
  128. # `copyfile` API available since osx 10.5.
  129. {.push nodecl, header: "<copyfile.h>".}
  130. type
  131. copyfile_state_t {.nodecl.} = pointer
  132. copyfile_flags_t = cint
  133. proc copyfile_state_alloc(): copyfile_state_t
  134. proc copyfile_state_free(state: copyfile_state_t): cint
  135. proc c_copyfile(src, dst: cstring, state: copyfile_state_t, flags: copyfile_flags_t): cint {.importc: "copyfile".}
  136. when (NimMajor, NimMinor) >= (1, 4):
  137. let
  138. COPYFILE_DATA {.nodecl.}: copyfile_flags_t
  139. COPYFILE_XATTR {.nodecl.}: copyfile_flags_t
  140. else:
  141. var
  142. COPYFILE_DATA {.nodecl.}: copyfile_flags_t
  143. COPYFILE_XATTR {.nodecl.}: copyfile_flags_t
  144. {.pop.}
  145. type
  146. CopyFlag* = enum ## Copy options.
  147. cfSymlinkAsIs, ## Copy symlinks as symlinks
  148. cfSymlinkFollow, ## Copy the files symlinks point to
  149. cfSymlinkIgnore ## Ignore symlinks
  150. const copyFlagSymlink = {cfSymlinkAsIs, cfSymlinkFollow, cfSymlinkIgnore}
  151. proc copyFile*(source, dest: string, options = {cfSymlinkFollow}; bufferSize = 16_384) {.rtl,
  152. extern: "nos$1", tags: [ReadDirEffect, ReadIOEffect, WriteIOEffect],
  153. noWeirdTarget.} =
  154. ## Copies a file from `source` to `dest`, where `dest.parentDir` must exist.
  155. ##
  156. ## On non-Windows OSes, `options` specify the way file is copied; by default,
  157. ## if `source` is a symlink, copies the file symlink points to. `options` is
  158. ## ignored on Windows: symlinks are skipped.
  159. ##
  160. ## If this fails, `OSError` is raised.
  161. ##
  162. ## On the Windows platform this proc will
  163. ## copy the source file's attributes into dest.
  164. ##
  165. ## On other platforms you need
  166. ## to use `getFilePermissions`_ and
  167. ## `setFilePermissions`_
  168. ## procs
  169. ## to copy them by hand (or use the convenience `copyFileWithPermissions
  170. ## proc`_),
  171. ## otherwise `dest` will inherit the default permissions of a newly
  172. ## created file for the user.
  173. ##
  174. ## If `dest` already exists, the file attributes
  175. ## will be preserved and the content overwritten.
  176. ##
  177. ## On OSX, `copyfile` C api will be used (available since OSX 10.5) unless
  178. ## `-d:nimLegacyCopyFile` is used.
  179. ##
  180. ## `copyFile` allows to specify `bufferSize` to improve I/O performance.
  181. ##
  182. ## See also:
  183. ## * `CopyFlag enum`_
  184. ## * `copyDir proc`_
  185. ## * `copyFileWithPermissions proc`_
  186. ## * `tryRemoveFile proc`_
  187. ## * `removeFile proc`_
  188. ## * `moveFile proc`_
  189. doAssert card(copyFlagSymlink * options) == 1, "There should be exactly one cfSymlink* in options"
  190. let isSymlink = source.symlinkExists
  191. if isSymlink and (cfSymlinkIgnore in options or defined(windows)):
  192. return
  193. when defined(windows):
  194. let s = newWideCString(source)
  195. let d = newWideCString(dest)
  196. if copyFileW(s, d, 0'i32) == 0'i32:
  197. raiseOSError(osLastError(), $(source, dest))
  198. else:
  199. if isSymlink and cfSymlinkAsIs in options:
  200. createSymlink(expandSymlink(source), dest)
  201. else:
  202. when hasCCopyfile:
  203. let state = copyfile_state_alloc()
  204. # xxx `COPYFILE_STAT` could be used for one-shot
  205. # `copyFileWithPermissions`.
  206. let status = c_copyfile(source.cstring, dest.cstring, state,
  207. COPYFILE_DATA)
  208. if status != 0:
  209. let err = osLastError()
  210. discard copyfile_state_free(state)
  211. raiseOSError(err, $(source, dest))
  212. let status2 = copyfile_state_free(state)
  213. if status2 != 0: raiseOSError(osLastError(), $(source, dest))
  214. else:
  215. # generic version of copyFile which works for any platform:
  216. var d, s: File
  217. if not open(s, source): raiseOSError(osLastError(), source)
  218. if not open(d, dest, fmWrite):
  219. close(s)
  220. raiseOSError(osLastError(), dest)
  221. # Hints for kernel-level aggressive sequential low-fragmentation read-aheads:
  222. # https://pubs.opengroup.org/onlinepubs/9699919799/functions/posix_fadvise.html
  223. when defined(linux) or defined(osx):
  224. discard posix_fadvise(getFileHandle(d), 0.cint, 0.cint, POSIX_FADV_SEQUENTIAL)
  225. discard posix_fadvise(getFileHandle(s), 0.cint, 0.cint, POSIX_FADV_SEQUENTIAL)
  226. var buf = alloc(bufferSize)
  227. while true:
  228. var bytesread = readBuffer(s, buf, bufferSize)
  229. if bytesread > 0:
  230. var byteswritten = writeBuffer(d, buf, bytesread)
  231. if bytesread != byteswritten:
  232. dealloc(buf)
  233. close(s)
  234. close(d)
  235. raiseOSError(osLastError(), dest)
  236. if bytesread != bufferSize: break
  237. dealloc(buf)
  238. close(s)
  239. flushFile(d)
  240. close(d)
  241. proc copyFileToDir*(source, dir: string, options = {cfSymlinkFollow}; bufferSize = 16_384)
  242. {.noWeirdTarget, since: (1,3,7).} =
  243. ## Copies a file `source` into directory `dir`, which must exist.
  244. ##
  245. ## On non-Windows OSes, `options` specify the way file is copied; by default,
  246. ## if `source` is a symlink, copies the file symlink points to. `options` is
  247. ## ignored on Windows: symlinks are skipped.
  248. ##
  249. ## `copyFileToDir` allows to specify `bufferSize` to improve I/O performance.
  250. ##
  251. ## See also:
  252. ## * `CopyFlag enum`_
  253. ## * `copyFile proc`_
  254. if dir.len == 0: # treating "" as "." is error prone
  255. raise newException(ValueError, "dest is empty")
  256. copyFile(source, dir / source.lastPathPart, options, bufferSize)
  257. proc copyFileWithPermissions*(source, dest: string,
  258. ignorePermissionErrors = true,
  259. options = {cfSymlinkFollow}) {.noWeirdTarget.} =
  260. ## Copies a file from `source` to `dest` preserving file permissions.
  261. ##
  262. ## On non-Windows OSes, `options` specify the way file is copied; by default,
  263. ## if `source` is a symlink, copies the file symlink points to. `options` is
  264. ## ignored on Windows: symlinks are skipped.
  265. ##
  266. ## This is a wrapper proc around `copyFile`_,
  267. ## `getFilePermissions`_ and `setFilePermissions`_
  268. ## procs on non-Windows platforms.
  269. ##
  270. ## On Windows this proc is just a wrapper for `copyFile proc`_ since
  271. ## that proc already copies attributes.
  272. ##
  273. ## On non-Windows systems permissions are copied after the file itself has
  274. ## been copied, which won't happen atomically and could lead to a race
  275. ## condition. If `ignorePermissionErrors` is true (default), errors while
  276. ## reading/setting file attributes will be ignored, otherwise will raise
  277. ## `OSError`.
  278. ##
  279. ## See also:
  280. ## * `CopyFlag enum`_
  281. ## * `copyFile proc`_
  282. ## * `copyDir proc`_
  283. ## * `tryRemoveFile proc`_
  284. ## * `removeFile proc`_
  285. ## * `moveFile proc`_
  286. ## * `copyDirWithPermissions proc`_
  287. copyFile(source, dest, options)
  288. when not defined(windows):
  289. try:
  290. setFilePermissions(dest, getFilePermissions(source), followSymlinks =
  291. (cfSymlinkFollow in options))
  292. except:
  293. if not ignorePermissionErrors:
  294. raise
  295. when not declared(ENOENT) and not defined(windows):
  296. when defined(nimscript):
  297. when not defined(haiku):
  298. const ENOENT = cint(2) # 2 on most systems including Solaris
  299. else:
  300. const ENOENT = cint(-2147459069)
  301. else:
  302. var ENOENT {.importc, header: "<errno.h>".}: cint
  303. when defined(windows) and not weirdTarget:
  304. template deleteFile(file: untyped): untyped = deleteFileW(file)
  305. template setFileAttributes(file, attrs: untyped): untyped =
  306. setFileAttributesW(file, attrs)
  307. proc tryRemoveFile*(file: string): bool {.rtl, extern: "nos$1", tags: [WriteDirEffect], noWeirdTarget.} =
  308. ## Removes the `file`.
  309. ##
  310. ## If this fails, returns `false`. This does not fail
  311. ## if the file never existed in the first place.
  312. ##
  313. ## On Windows, ignores the read-only attribute.
  314. ##
  315. ## See also:
  316. ## * `copyFile proc`_
  317. ## * `copyFileWithPermissions proc`_
  318. ## * `removeFile proc`_
  319. ## * `moveFile proc`_
  320. result = true
  321. when defined(windows):
  322. let f = newWideCString(file)
  323. if deleteFile(f) == 0:
  324. result = false
  325. let err = getLastError()
  326. if err == ERROR_FILE_NOT_FOUND or err == ERROR_PATH_NOT_FOUND:
  327. result = true
  328. elif err == ERROR_ACCESS_DENIED and
  329. setFileAttributes(f, FILE_ATTRIBUTE_NORMAL) != 0 and
  330. deleteFile(f) != 0:
  331. result = true
  332. else:
  333. if unlink(file) != 0'i32 and errno != ENOENT:
  334. result = false
  335. proc removeFile*(file: string) {.rtl, extern: "nos$1", tags: [WriteDirEffect], noWeirdTarget.} =
  336. ## Removes the `file`.
  337. ##
  338. ## If this fails, `OSError` is raised. This does not fail
  339. ## if the file never existed in the first place.
  340. ##
  341. ## On Windows, ignores the read-only attribute.
  342. ##
  343. ## See also:
  344. ## * `removeDir proc`_
  345. ## * `copyFile proc`_
  346. ## * `copyFileWithPermissions proc`_
  347. ## * `tryRemoveFile proc`_
  348. ## * `moveFile proc`_
  349. if not tryRemoveFile(file):
  350. raiseOSError(osLastError(), file)
  351. proc moveFile*(source, dest: string) {.rtl, extern: "nos$1",
  352. tags: [ReadDirEffect, ReadIOEffect, WriteIOEffect], noWeirdTarget.} =
  353. ## Moves a file from `source` to `dest`.
  354. ##
  355. ## Symlinks are not followed: if `source` is a symlink, it is itself moved,
  356. ## not its target.
  357. ##
  358. ## If this fails, `OSError` is raised.
  359. ## If `dest` already exists, it will be overwritten.
  360. ##
  361. ## Can be used to `rename files`:idx:.
  362. ##
  363. ## See also:
  364. ## * `moveDir proc`_
  365. ## * `copyFile proc`_
  366. ## * `copyFileWithPermissions proc`_
  367. ## * `removeFile proc`_
  368. ## * `tryRemoveFile proc`_
  369. if not tryMoveFSObject(source, dest, isDir = false):
  370. when defined(windows):
  371. raiseAssert "unreachable"
  372. else:
  373. # Fallback to copy & del
  374. copyFileWithPermissions(source, dest, options={cfSymlinkAsIs})
  375. try:
  376. removeFile(source)
  377. except:
  378. discard tryRemoveFile(dest)
  379. raise