osdirs.nim 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. ## .. importdoc:: osfiles.nim, appdirs.nim, paths.nim
  2. include system/inclrtl
  3. import std/oserrors
  4. import ospaths2, osfiles
  5. import oscommon
  6. export dirExists, PathComponent
  7. when defined(nimPreviewSlimSystem):
  8. import std/[syncio, assertions, widestrs]
  9. when weirdTarget:
  10. discard
  11. elif defined(windows):
  12. import std/[winlean, times]
  13. elif defined(posix):
  14. import std/[posix, times]
  15. else:
  16. {.error: "OS module not ported to your operating system!".}
  17. when weirdTarget:
  18. {.pragma: noWeirdTarget, error: "this proc is not available on the NimScript/js target".}
  19. else:
  20. {.pragma: noWeirdTarget.}
  21. when defined(nimscript):
  22. # for procs already defined in scriptconfig.nim
  23. template noNimJs(body): untyped = discard
  24. elif defined(js):
  25. {.pragma: noNimJs, error: "this proc is not available on the js target".}
  26. else:
  27. {.pragma: noNimJs.}
  28. # Templates for filtering directories and files
  29. when defined(windows) and not weirdTarget:
  30. template isDir(f: WIN32_FIND_DATA): bool =
  31. (f.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY) != 0'i32
  32. template isFile(f: WIN32_FIND_DATA): bool =
  33. not isDir(f)
  34. else:
  35. template isDir(f: string): bool {.dirty.} =
  36. dirExists(f)
  37. template isFile(f: string): bool {.dirty.} =
  38. fileExists(f)
  39. template defaultWalkFilter(item): bool =
  40. ## Walk filter used to return true on both
  41. ## files and directories
  42. true
  43. template walkCommon(pattern: string, filter) =
  44. ## Common code for getting the files and directories with the
  45. ## specified `pattern`
  46. when defined(windows):
  47. var
  48. f: WIN32_FIND_DATA
  49. res: int
  50. res = findFirstFile(pattern, f)
  51. if res != -1:
  52. defer: findClose(res)
  53. let dotPos = searchExtPos(pattern)
  54. while true:
  55. if not skipFindData(f) and filter(f):
  56. # Windows bug/gotcha: 't*.nim' matches 'tfoo.nims' -.- so we check
  57. # that the file extensions have the same length ...
  58. let ff = getFilename(f)
  59. let idx = ff.len - pattern.len + dotPos
  60. if dotPos < 0 or idx >= ff.len or (idx >= 0 and ff[idx] == '.') or
  61. (dotPos >= 0 and dotPos+1 < pattern.len and pattern[dotPos+1] == '*'):
  62. yield splitFile(pattern).dir / extractFilename(ff)
  63. if findNextFile(res, f) == 0'i32:
  64. let errCode = getLastError()
  65. if errCode == ERROR_NO_MORE_FILES: break
  66. else: raiseOSError(errCode.OSErrorCode)
  67. else: # here we use glob
  68. var
  69. f: Glob
  70. res: int
  71. f.gl_offs = 0
  72. f.gl_pathc = 0
  73. f.gl_pathv = nil
  74. res = glob(pattern, 0, nil, addr(f))
  75. defer: globfree(addr(f))
  76. if res == 0:
  77. for i in 0.. f.gl_pathc - 1:
  78. assert(f.gl_pathv[i] != nil)
  79. let path = $f.gl_pathv[i]
  80. if filter(path):
  81. yield path
  82. iterator walkPattern*(pattern: string): string {.tags: [ReadDirEffect], noWeirdTarget.} =
  83. ## Iterate over all the files and directories that match the `pattern`.
  84. ##
  85. ## On POSIX this uses the `glob`:idx: call.
  86. ## `pattern` is OS dependent, but at least the `"*.ext"`
  87. ## notation is supported.
  88. ##
  89. ## See also:
  90. ## * `walkFiles iterator`_
  91. ## * `walkDirs iterator`_
  92. ## * `walkDir iterator`_
  93. ## * `walkDirRec iterator`_
  94. runnableExamples:
  95. import std/os
  96. import std/sequtils
  97. let paths = toSeq(walkPattern("lib/pure/*")) # works on Windows too
  98. assert "lib/pure/concurrency".unixToNativePath in paths
  99. assert "lib/pure/os.nim".unixToNativePath in paths
  100. walkCommon(pattern, defaultWalkFilter)
  101. iterator walkFiles*(pattern: string): string {.tags: [ReadDirEffect], noWeirdTarget.} =
  102. ## Iterate over all the files that match the `pattern`.
  103. ##
  104. ## On POSIX this uses the `glob`:idx: call.
  105. ## `pattern` is OS dependent, but at least the `"*.ext"`
  106. ## notation is supported.
  107. ##
  108. ## See also:
  109. ## * `walkPattern iterator`_
  110. ## * `walkDirs iterator`_
  111. ## * `walkDir iterator`_
  112. ## * `walkDirRec iterator`_
  113. runnableExamples:
  114. import std/os
  115. import std/sequtils
  116. assert "lib/pure/os.nim".unixToNativePath in toSeq(walkFiles("lib/pure/*.nim")) # works on Windows too
  117. walkCommon(pattern, isFile)
  118. iterator walkDirs*(pattern: string): string {.tags: [ReadDirEffect], noWeirdTarget.} =
  119. ## Iterate over all the directories that match the `pattern`.
  120. ##
  121. ## On POSIX this uses the `glob`:idx: call.
  122. ## `pattern` is OS dependent, but at least the `"*.ext"`
  123. ## notation is supported.
  124. ##
  125. ## See also:
  126. ## * `walkPattern iterator`_
  127. ## * `walkFiles iterator`_
  128. ## * `walkDir iterator`_
  129. ## * `walkDirRec iterator`_
  130. runnableExamples:
  131. import std/os
  132. import std/sequtils
  133. let paths = toSeq(walkDirs("lib/pure/*")) # works on Windows too
  134. assert "lib/pure/concurrency".unixToNativePath in paths
  135. walkCommon(pattern, isDir)
  136. proc staticWalkDir(dir: string; relative: bool): seq[
  137. tuple[kind: PathComponent, path: string]] =
  138. discard
  139. iterator walkDir*(dir: string; relative = false, checkDir = false,
  140. skipSpecial = false):
  141. tuple[kind: PathComponent, path: string] {.tags: [ReadDirEffect].} =
  142. ## Walks over the directory `dir` and yields for each directory or file in
  143. ## `dir`. The component type and full path for each item are returned.
  144. ##
  145. ## Walking is not recursive.
  146. ## * If `relative` is true (default: false)
  147. ## the resulting path is shortened to be relative to ``dir``,
  148. ## otherwise the full path is returned.
  149. ## * If `checkDir` is true, `OSError` is raised when `dir`
  150. ## doesn't exist.
  151. ## * If `skipSpecial` is true, then (besides all directories) only *regular*
  152. ## files (**without** special "file" objects like FIFOs, device files,
  153. ## etc) will be yielded on Unix.
  154. ##
  155. ## **Example:**
  156. ##
  157. ## This directory structure:
  158. ##
  159. ## dirA / dirB / fileB1.txt
  160. ## / dirC
  161. ## / fileA1.txt
  162. ## / fileA2.txt
  163. ##
  164. ## and this code:
  165. runnableExamples("-r:off"):
  166. import std/[strutils, sugar]
  167. # note: order is not guaranteed
  168. # this also works at compile time
  169. assert collect(for k in walkDir("dirA"): k.path).join(" ") ==
  170. "dirA/dirB dirA/dirC dirA/fileA2.txt dirA/fileA1.txt"
  171. ## See also:
  172. ## * `walkPattern iterator`_
  173. ## * `walkFiles iterator`_
  174. ## * `walkDirs iterator`_
  175. ## * `walkDirRec iterator`_
  176. when nimvm:
  177. for k, v in items(staticWalkDir(dir, relative)):
  178. yield (k, v)
  179. else:
  180. when weirdTarget:
  181. for k, v in items(staticWalkDir(dir, relative)):
  182. yield (k, v)
  183. elif defined(windows):
  184. var f: WIN32_FIND_DATA
  185. var h = findFirstFile(dir / "*", f)
  186. if h == -1:
  187. if checkDir:
  188. raiseOSError(osLastError(), dir)
  189. else:
  190. defer: findClose(h)
  191. while true:
  192. var k = pcFile
  193. if not skipFindData(f):
  194. if (f.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY) != 0'i32:
  195. k = pcDir
  196. if (f.dwFileAttributes and FILE_ATTRIBUTE_REPARSE_POINT) != 0'i32:
  197. k = succ(k)
  198. let xx = if relative: extractFilename(getFilename(f))
  199. else: dir / extractFilename(getFilename(f))
  200. yield (k, xx)
  201. if findNextFile(h, f) == 0'i32:
  202. let errCode = getLastError()
  203. if errCode == ERROR_NO_MORE_FILES: break
  204. else: raiseOSError(errCode.OSErrorCode)
  205. else:
  206. var d = opendir(dir)
  207. if d == nil:
  208. if checkDir:
  209. raiseOSError(osLastError(), dir)
  210. else:
  211. defer: discard closedir(d)
  212. while true:
  213. var x = readdir(d)
  214. if x == nil: break
  215. var y = $cast[cstring](addr x.d_name)
  216. if y != "." and y != "..":
  217. var s: Stat
  218. let path = dir / y
  219. if not relative:
  220. y = path
  221. var k = pcFile
  222. template resolveSymlink() =
  223. var isSpecial: bool
  224. (k, isSpecial) = getSymlinkFileKind(path)
  225. if skipSpecial and isSpecial: continue
  226. template kSetGeneric() = # pure Posix component `k` resolution
  227. if lstat(path.cstring, s) < 0'i32: continue # don't yield
  228. elif S_ISDIR(s.st_mode):
  229. k = pcDir
  230. elif S_ISLNK(s.st_mode):
  231. resolveSymlink()
  232. elif skipSpecial and not S_ISREG(s.st_mode): continue
  233. when defined(linux) or defined(macosx) or
  234. defined(bsd) or defined(genode) or defined(nintendoswitch):
  235. case x.d_type
  236. of DT_DIR: k = pcDir
  237. of DT_LNK:
  238. resolveSymlink()
  239. of DT_UNKNOWN:
  240. kSetGeneric()
  241. else: # DT_REG or special "files" like FIFOs
  242. if skipSpecial and x.d_type != DT_REG: continue
  243. else: discard # leave it as pcFile
  244. else: # assuming that field `d_type` is not present
  245. kSetGeneric()
  246. yield (k, y)
  247. iterator walkDirRec*(dir: string,
  248. yieldFilter = {pcFile}, followFilter = {pcDir},
  249. relative = false, checkDir = false, skipSpecial = false):
  250. string {.tags: [ReadDirEffect].} =
  251. ## Recursively walks over the directory `dir` and yields for each file
  252. ## or directory in `dir`.
  253. ##
  254. ## Options `relative`, `checkdir`, `skipSpecial` are explained in
  255. ## [walkDir iterator] description.
  256. ##
  257. ## .. warning:: Modifying the directory structure while the iterator
  258. ## is traversing may result in undefined behavior!
  259. ##
  260. ## Walking is recursive. `followFilter` controls the behaviour of the iterator:
  261. ##
  262. ## ===================== =============================================
  263. ## yieldFilter meaning
  264. ## ===================== =============================================
  265. ## ``pcFile`` yield real files (default)
  266. ## ``pcLinkToFile`` yield symbolic links to files
  267. ## ``pcDir`` yield real directories
  268. ## ``pcLinkToDir`` yield symbolic links to directories
  269. ## ===================== =============================================
  270. ##
  271. ## ===================== =============================================
  272. ## followFilter meaning
  273. ## ===================== =============================================
  274. ## ``pcDir`` follow real directories (default)
  275. ## ``pcLinkToDir`` follow symbolic links to directories
  276. ## ===================== =============================================
  277. ##
  278. ##
  279. ## See also:
  280. ## * `walkPattern iterator`_
  281. ## * `walkFiles iterator`_
  282. ## * `walkDirs iterator`_
  283. ## * `walkDir iterator`_
  284. var stack = @[""]
  285. var checkDir = checkDir
  286. while stack.len > 0:
  287. let d = stack.pop()
  288. for k, p in walkDir(dir / d, relative = true, checkDir = checkDir,
  289. skipSpecial = skipSpecial):
  290. let rel = d / p
  291. if k in {pcDir, pcLinkToDir} and k in followFilter:
  292. stack.add rel
  293. if k in yieldFilter:
  294. yield if relative: rel else: dir / rel
  295. checkDir = false
  296. # We only check top-level dir, otherwise if a subdir is invalid (eg. wrong
  297. # permissions), it'll abort iteration and there would be no way to
  298. # continue iteration.
  299. # Future work can provide a way to customize this and do error reporting.
  300. proc rawRemoveDir(dir: string) {.noWeirdTarget.} =
  301. when defined(windows):
  302. wrapUnary(res, removeDirectoryW, dir)
  303. let lastError = osLastError()
  304. if res == 0'i32 and lastError.int32 != 3'i32 and
  305. lastError.int32 != 18'i32 and lastError.int32 != 2'i32:
  306. raiseOSError(lastError, dir)
  307. else:
  308. if rmdir(dir) != 0'i32 and errno != ENOENT: raiseOSError(osLastError(), dir)
  309. proc removeDir*(dir: string, checkDir = false) {.rtl, extern: "nos$1", tags: [
  310. WriteDirEffect, ReadDirEffect], benign, noWeirdTarget.} =
  311. ## Removes the directory `dir` including all subdirectories and files
  312. ## in `dir` (recursively).
  313. ##
  314. ## If this fails, `OSError` is raised. This does not fail if the directory never
  315. ## existed in the first place, unless `checkDir` = true.
  316. ##
  317. ## See also:
  318. ## * `tryRemoveFile proc`_
  319. ## * `removeFile proc`_
  320. ## * `existsOrCreateDir proc`_
  321. ## * `createDir proc`_
  322. ## * `copyDir proc`_
  323. ## * `copyDirWithPermissions proc`_
  324. ## * `moveDir proc`_
  325. for kind, path in walkDir(dir, checkDir = checkDir):
  326. case kind
  327. of pcFile, pcLinkToFile, pcLinkToDir: removeFile(path)
  328. of pcDir: removeDir(path, true)
  329. # for subdirectories there is no benefit in `checkDir = false`
  330. # (unless perhaps for edge case of concurrent processes also deleting
  331. # the same files)
  332. rawRemoveDir(dir)
  333. proc rawCreateDir(dir: string): bool {.noWeirdTarget.} =
  334. # Try to create one directory (not the whole path).
  335. # returns `true` for success, `false` if the path has previously existed
  336. #
  337. # This is a thin wrapper over mkDir (or alternatives on other systems),
  338. # so in case of a pre-existing path we don't check that it is a directory.
  339. when defined(solaris):
  340. let res = mkdir(dir, 0o777)
  341. if res == 0'i32:
  342. result = true
  343. elif errno in {EEXIST, ENOSYS}:
  344. result = false
  345. else:
  346. raiseOSError(osLastError(), dir)
  347. elif defined(haiku):
  348. let res = mkdir(dir, 0o777)
  349. if res == 0'i32:
  350. result = true
  351. elif errno == EEXIST or errno == EROFS:
  352. result = false
  353. else:
  354. raiseOSError(osLastError(), dir)
  355. elif defined(posix):
  356. let res = mkdir(dir, 0o777)
  357. if res == 0'i32:
  358. result = true
  359. elif errno == EEXIST:
  360. result = false
  361. else:
  362. #echo res
  363. raiseOSError(osLastError(), dir)
  364. else:
  365. wrapUnary(res, createDirectoryW, dir)
  366. if res != 0'i32:
  367. result = true
  368. elif getLastError() == 183'i32:
  369. result = false
  370. else:
  371. raiseOSError(osLastError(), dir)
  372. proc existsOrCreateDir*(dir: string): bool {.rtl, extern: "nos$1",
  373. tags: [WriteDirEffect, ReadDirEffect], noWeirdTarget.} =
  374. ## Checks if a `directory`:idx: `dir` exists, and creates it otherwise.
  375. ##
  376. ## Does not create parent directories (raises `OSError` if parent directories do not exist).
  377. ## Returns `true` if the directory already exists, and `false` otherwise.
  378. ##
  379. ## See also:
  380. ## * `removeDir proc`_
  381. ## * `createDir proc`_
  382. ## * `copyDir proc`_
  383. ## * `copyDirWithPermissions proc`_
  384. ## * `moveDir proc`_
  385. result = not rawCreateDir(dir)
  386. if result:
  387. # path already exists - need to check that it is indeed a directory
  388. if not dirExists(dir):
  389. raise newException(IOError, "Failed to create '" & dir & "'")
  390. proc createDir*(dir: string) {.rtl, extern: "nos$1",
  391. tags: [WriteDirEffect, ReadDirEffect], noWeirdTarget.} =
  392. ## Creates the `directory`:idx: `dir`.
  393. ##
  394. ## The directory may contain several subdirectories that do not exist yet.
  395. ## The full path is created. If this fails, `OSError` is raised.
  396. ##
  397. ## It does **not** fail if the directory already exists because for
  398. ## most usages this does not indicate an error.
  399. ##
  400. ## See also:
  401. ## * `removeDir proc`_
  402. ## * `existsOrCreateDir proc`_
  403. ## * `copyDir proc`_
  404. ## * `copyDirWithPermissions proc`_
  405. ## * `moveDir proc`_
  406. if dir == "":
  407. return
  408. var omitNext = isAbsolute(dir)
  409. for p in parentDirs(dir, fromRoot=true):
  410. if omitNext:
  411. omitNext = false
  412. else:
  413. discard existsOrCreateDir(p)
  414. proc copyDir*(source, dest: string) {.rtl, extern: "nos$1",
  415. tags: [ReadDirEffect, WriteIOEffect, ReadIOEffect], benign, noWeirdTarget.} =
  416. ## Copies a directory from `source` to `dest`.
  417. ##
  418. ## On non-Windows OSes, symlinks are copied as symlinks. On Windows, symlinks
  419. ## are skipped.
  420. ##
  421. ## If this fails, `OSError` is raised.
  422. ##
  423. ## On the Windows platform this proc will copy the attributes from
  424. ## `source` into `dest`.
  425. ##
  426. ## On other platforms created files and directories will inherit the
  427. ## default permissions of a newly created file/directory for the user.
  428. ## Use `copyDirWithPermissions proc`_
  429. ## to preserve attributes recursively on these platforms.
  430. ##
  431. ## See also:
  432. ## * `copyDirWithPermissions proc`_
  433. ## * `copyFile proc`_
  434. ## * `copyFileWithPermissions proc`_
  435. ## * `removeDir proc`_
  436. ## * `existsOrCreateDir proc`_
  437. ## * `createDir proc`_
  438. ## * `moveDir proc`_
  439. createDir(dest)
  440. for kind, path in walkDir(source):
  441. var noSource = splitPath(path).tail
  442. if kind == pcDir:
  443. copyDir(path, dest / noSource)
  444. else:
  445. copyFile(path, dest / noSource, {cfSymlinkAsIs})
  446. proc copyDirWithPermissions*(source, dest: string,
  447. ignorePermissionErrors = true)
  448. {.rtl, extern: "nos$1", tags: [ReadDirEffect, WriteIOEffect, ReadIOEffect],
  449. benign, noWeirdTarget.} =
  450. ## Copies a directory from `source` to `dest` preserving file permissions.
  451. ##
  452. ## On non-Windows OSes, symlinks are copied as symlinks. On Windows, symlinks
  453. ## are skipped.
  454. ##
  455. ## If this fails, `OSError` is raised. This is a wrapper proc around
  456. ## `copyDir`_ and `copyFileWithPermissions`_ procs
  457. ## on non-Windows platforms.
  458. ##
  459. ## On Windows this proc is just a wrapper for `copyDir proc`_ since
  460. ## that proc already copies attributes.
  461. ##
  462. ## On non-Windows systems permissions are copied after the file or directory
  463. ## itself has been copied, which won't happen atomically and could lead to a
  464. ## race condition. If `ignorePermissionErrors` is true (default), errors while
  465. ## reading/setting file attributes will be ignored, otherwise will raise
  466. ## `OSError`.
  467. ##
  468. ## See also:
  469. ## * `copyDir proc`_
  470. ## * `copyFile proc`_
  471. ## * `copyFileWithPermissions proc`_
  472. ## * `removeDir proc`_
  473. ## * `moveDir proc`_
  474. ## * `existsOrCreateDir proc`_
  475. ## * `createDir proc`_
  476. createDir(dest)
  477. when not defined(windows):
  478. try:
  479. setFilePermissions(dest, getFilePermissions(source), followSymlinks =
  480. false)
  481. except:
  482. if not ignorePermissionErrors:
  483. raise
  484. for kind, path in walkDir(source):
  485. var noSource = splitPath(path).tail
  486. if kind == pcDir:
  487. copyDirWithPermissions(path, dest / noSource, ignorePermissionErrors)
  488. else:
  489. copyFileWithPermissions(path, dest / noSource, ignorePermissionErrors, {cfSymlinkAsIs})
  490. proc moveDir*(source, dest: string) {.tags: [ReadIOEffect, WriteIOEffect], noWeirdTarget.} =
  491. ## Moves a directory from `source` to `dest`.
  492. ##
  493. ## Symlinks are not followed: if `source` contains symlinks, they themself are
  494. ## moved, not their target.
  495. ##
  496. ## If this fails, `OSError` is raised.
  497. ##
  498. ## See also:
  499. ## * `moveFile proc`_
  500. ## * `copyDir proc`_
  501. ## * `copyDirWithPermissions proc`_
  502. ## * `removeDir proc`_
  503. ## * `existsOrCreateDir proc`_
  504. ## * `createDir proc`_
  505. if not tryMoveFSObject(source, dest, isDir = true):
  506. # Fallback to copy & del
  507. copyDir(source, dest)
  508. removeDir(source)
  509. proc setCurrentDir*(newDir: string) {.inline, tags: [], noWeirdTarget.} =
  510. ## Sets the `current working directory`:idx:; `OSError`
  511. ## is raised if `newDir` cannot been set.
  512. ##
  513. ## See also:
  514. ## * `getHomeDir proc`_
  515. ## * `getConfigDir proc`_
  516. ## * `getTempDir proc`_
  517. ## * `getCurrentDir proc`_
  518. when defined(windows):
  519. if setCurrentDirectoryW(newWideCString(newDir)) == 0'i32:
  520. raiseOSError(osLastError(), newDir)
  521. else:
  522. if chdir(newDir) != 0'i32: raiseOSError(osLastError(), newDir)