tempfiles.nim 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. #
  2. #
  3. # Nim's Runtime Library
  4. # (c) Copyright 2021 Nim contributors
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. ## This module creates temporary files and directories.
  10. ##
  11. ## Experimental API, subject to change.
  12. #[
  13. See also:
  14. * `GetTempFileName` (on windows), refs https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettempfilenamea
  15. * `mkstemp` (posix), refs https://man7.org/linux/man-pages/man3/mkstemp.3.html
  16. ]#
  17. import os, random
  18. when defined(nimPreviewSlimSystem):
  19. import std/syncio
  20. const
  21. maxRetry = 10000
  22. letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
  23. nimTempPathLength {.intdefine.} = 8
  24. when defined(windows):
  25. import winlean
  26. when defined(nimPreviewSlimSystem):
  27. import std/widestrs
  28. var O_RDWR {.importc: "_O_RDWR", header: "<fcntl.h>".}: cint
  29. proc c_fdopen(
  30. filehandle: cint,
  31. mode: cstring
  32. ): File {.importc: "_fdopen",header: "<stdio.h>".}
  33. proc open_osfhandle(osh: Handle, mode: cint): cint {.
  34. importc: "_open_osfhandle", header: "<io.h>".}
  35. proc close_osfandle(fd: cint): cint {.
  36. importc: "_close", header: "<io.h>".}
  37. else:
  38. import posix
  39. proc c_fdopen(
  40. filehandle: cint,
  41. mode: cstring
  42. ): File {.importc: "fdopen",header: "<stdio.h>".}
  43. proc safeOpen(filename: string): File =
  44. ## Open files exclusively; returns `nil` if the file already exists.
  45. # xxx this should be clarified; it doesn't in particular prevent other processes
  46. # from opening the file, at least currently.
  47. when defined(windows):
  48. let dwShareMode = FILE_SHARE_DELETE or FILE_SHARE_READ or FILE_SHARE_WRITE
  49. let dwCreation = CREATE_NEW
  50. let dwFlags = FILE_FLAG_BACKUP_SEMANTICS or FILE_ATTRIBUTE_NORMAL
  51. let handle = createFileW(newWideCString(filename), GENERIC_READ or GENERIC_WRITE, dwShareMode,
  52. nil, dwCreation, dwFlags, Handle(0))
  53. if handle == INVALID_HANDLE_VALUE:
  54. if getLastError() == ERROR_FILE_EXISTS:
  55. return nil
  56. else:
  57. raiseOSError(osLastError(), filename)
  58. let fileHandle = open_osfhandle(handle, O_RDWR)
  59. if fileHandle == -1:
  60. discard closeHandle(handle)
  61. raiseOSError(osLastError(), filename)
  62. result = c_fdopen(fileHandle, "w+")
  63. if result == nil:
  64. discard close_osfandle(fileHandle)
  65. raiseOSError(osLastError(), filename)
  66. else:
  67. # xxx we need a `proc toMode(a: FilePermission): Mode`, possibly by
  68. # exposing fusion/filepermissions.fromFilePermissions to stdlib; then we need
  69. # to expose a `perm` param so users can customize this (e.g. the temp file may
  70. # need execute permissions), and figure out how to make the API cross platform.
  71. let mode = Mode(S_IRUSR or S_IWUSR)
  72. let flags = posix.O_RDWR or posix.O_CREAT or posix.O_EXCL
  73. let fileHandle = posix.open(filename, flags, mode)
  74. if fileHandle == -1:
  75. if errno == EEXIST:
  76. # xxx `getLastError()` should be defined on posix too and resolve to `errno`?
  77. return nil
  78. else:
  79. raiseOSError(osLastError(), filename)
  80. result = c_fdopen(fileHandle, "w+")
  81. if result == nil:
  82. discard posix.close(fileHandle) # TODO handles failure when closing file
  83. raiseOSError(osLastError(), filename)
  84. type
  85. NimTempPathState = object
  86. state: Rand
  87. isInit: bool
  88. var nimTempPathState {.threadvar.}: NimTempPathState
  89. template randomPathName(length: Natural): string =
  90. var res = newString(length)
  91. if not nimTempPathState.isInit:
  92. nimTempPathState.isInit = true
  93. nimTempPathState.state = initRand()
  94. for i in 0 ..< length:
  95. res[i] = nimTempPathState.state.sample(letters)
  96. res
  97. proc getTempDirImpl(dir: string): string {.inline.} =
  98. result = dir
  99. if result.len == 0:
  100. result = getTempDir()
  101. proc genTempPath*(prefix, suffix: string, dir = ""): string =
  102. ## Generates a path name in `dir`.
  103. ##
  104. ## The path begins with `prefix` and ends with `suffix`.
  105. ##
  106. ## .. note:: `dir` must exist (empty `dir` will resolve to `getTempDir <os.html#getTempDir>`_).
  107. let dir = getTempDirImpl(dir)
  108. result = dir / (prefix & randomPathName(nimTempPathLength) & suffix)
  109. proc createTempFile*(prefix, suffix: string, dir = ""): tuple[cfile: File, path: string] =
  110. ## Creates a new temporary file in the directory `dir`.
  111. ##
  112. ## This generates a path name using `genTempPath(prefix, suffix, dir)` and
  113. ## returns a file handle to an open file and the path of that file, possibly after
  114. ## retrying to ensure it doesn't already exist.
  115. ##
  116. ## If failing to create a temporary file, `OSError` will be raised.
  117. ##
  118. ## .. note:: It is the caller's responsibility to close `result.cfile` and
  119. ## remove `result.file` when no longer needed.
  120. ## .. note:: `dir` must exist (empty `dir` will resolve to `getTempDir <os.html#getTempDir>`_).
  121. runnableExamples:
  122. import std/os
  123. doAssertRaises(OSError): discard createTempFile("", "", "nonexistent")
  124. let (cfile, path) = createTempFile("tmpprefix_", "_end.tmp")
  125. # path looks like: getTempDir() / "tmpprefix_FDCIRZA0_end.tmp"
  126. cfile.write "foo"
  127. cfile.setFilePos 0
  128. assert readAll(cfile) == "foo"
  129. close cfile
  130. assert readFile(path) == "foo"
  131. removeFile(path)
  132. # xxx why does above work without `cfile.flushFile` ?
  133. let dir = getTempDirImpl(dir)
  134. for i in 0 ..< maxRetry:
  135. result.path = genTempPath(prefix, suffix, dir)
  136. result.cfile = safeOpen(result.path)
  137. if result.cfile != nil:
  138. return
  139. raise newException(OSError, "Failed to create a temporary file under directory " & dir)
  140. proc createTempDir*(prefix, suffix: string, dir = ""): string =
  141. ## Creates a new temporary directory in the directory `dir`.
  142. ##
  143. ## This generates a dir name using `genTempPath(prefix, suffix, dir)`, creates
  144. ## the directory and returns it, possibly after retrying to ensure it doesn't
  145. ## already exist.
  146. ##
  147. ## If failing to create a temporary directory, `OSError` will be raised.
  148. ##
  149. ## .. note:: It is the caller's responsibility to remove the directory when no longer needed.
  150. ## .. note:: `dir` must exist (empty `dir` will resolve to `getTempDir <os.html#getTempDir>`_).
  151. runnableExamples:
  152. import std/os
  153. doAssertRaises(OSError): discard createTempDir("", "", "nonexistent")
  154. let dir = createTempDir("tmpprefix_", "_end")
  155. # dir looks like: getTempDir() / "tmpprefix_YEl9VuVj_end"
  156. assert dirExists(dir)
  157. removeDir(dir)
  158. let dir = getTempDirImpl(dir)
  159. for i in 0 ..< maxRetry:
  160. result = genTempPath(prefix, suffix, dir)
  161. if not existsOrCreateDir(result):
  162. return
  163. raise newException(OSError, "Failed to create a temporary directory under directory " & dir)