osfiles.nim 15 KB

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