@@ -14,7 +14,13 @@ package sbt.io
1414import java .io ._
1515import java .net .{ URI , URISyntaxException , URL }
1616import java .nio .charset .Charset
17- import java .nio .file .attribute .PosixFilePermissions
17+ import java .nio .file .attribute .{
18+ AclEntry ,
19+ AclEntryPermission ,
20+ AclEntryType ,
21+ PosixFilePermissions ,
22+ UserPrincipal
23+ }
1824import java .nio .file .{ Path => NioPath , _ }
1925import java .util .{ Locale , Properties , UUID }
2026import java .util .concurrent .ForkJoinPool
@@ -475,7 +481,35 @@ object IO {
475481 * `write` returns successfully. If `write` throws, `to` is left untouched and the
476482 * staging file is removed.
477483 */
478- def writeFileAtomically [T ](to : File )(write : File => T ): T = {
484+ def writeFileAtomically [T ](to : File )(write : File => T ): T =
485+ writeFileAtomically(to, ownerOnly = false )(write)
486+
487+ /**
488+ * Stages a write to a sibling temp file and atomically replaces `to` only after
489+ * `write` returns successfully. If `write` throws, `to` is left untouched and the
490+ * staging file is removed.
491+ *
492+ * @param ownerOnly
493+ * if true, no content written out could be read by anyone other than its owner
494+ */
495+ def writeFileAtomically [T ](to : File , ownerOnly : Boolean )(write : File => T ): T =
496+ writeStaged(to, ownerOnly, replace = true )(write)
497+
498+ /**
499+ * Like `writeFileAtomically`, except that it refuses a `to` that already exists and
500+ * throws `FileAlreadyExistsException`.
501+ *
502+ * @param ownerOnly
503+ * if true, no content written out could be read by anyone other than its owner
504+ */
505+ def createFileAtomically [T ](to : File , ownerOnly : Boolean )(write : File => T ): T =
506+ writeStaged(to, ownerOnly, replace = false )(write)
507+
508+ private def writeStaged [T ](
509+ to : File ,
510+ ownerOnly : Boolean ,
511+ replace : Boolean
512+ )(write : File => T ): T = {
479513 val parent = Option (to.getAbsoluteFile.getParentFile).getOrElse(new File (" ." ))
480514 createDirectory(parent)
481515
@@ -488,7 +522,7 @@ object IO {
488522
489523 val toPath = to.toPath
490524 val staging = stagingFile.toPath
491- touch(stagingFile)
525+ if (ownerOnly) createForOwner(staging) else touch(stagingFile)
492526
493527 def retry (func : => NioPath ): NioPath = Retry (
494528 func,
@@ -498,19 +532,50 @@ object IO {
498532 )
499533 def move (options : CopyOption * ): NioPath = retry(Files .move(staging, toPath, options* ))
500534 def replaceFile (): NioPath =
535+ // ATOMIC_MOVE uses POSIX rename, so it can only be used with `replace`
501536 try move(StandardCopyOption .ATOMIC_MOVE , StandardCopyOption .REPLACE_EXISTING )
502537 catch { case _ : AtomicMoveNotSupportedException => move(StandardCopyOption .REPLACE_EXISTING ) }
538+ def createLink (): NioPath =
539+ // Files.move with ATOMIC_MOVE possibly replaces and without has a race condition
540+ try retry(Files .createLink(toPath, staging)) // try this first
541+ catch {
542+ case e @ (_ : UnsupportedOperationException | _ : IOException )
543+ if ! e.isInstanceOf [FileAlreadyExistsException ] =>
544+ move()
545+ }
503546
504547 try {
505548 val result = write(stagingFile)
506- replaceFile()
549+ if (replace)
550+ replaceFile()
551+ else
552+ createLink()
507553 result
508554 } finally {
509555 Files .deleteIfExists(staging)
510556 ()
511557 }
512558 }
513559
560+ /** Creates `path` such that only its owner can read and write it. */
561+ private def createForOwner (path : NioPath ): Unit = {
562+ if (isPosix) {
563+ val ownerOnly = PosixFilePermissions .fromString(" rw-------" )
564+ Files .createFile(path, PosixFilePermissions .asFileAttribute(ownerOnly))
565+ } else {
566+ Files .createFile(path)
567+ if (hasAclFileAttributeView) {
568+ val view = Path (path.toFile).aclFileAttributeView
569+ val acl = AclEntry .newBuilder
570+ acl.setPrincipal(view.getOwner)
571+ acl.setPermissions(AclEntryPermission .values()* )
572+ acl.setType(AclEntryType .ALLOW )
573+ view.setAcl(java.util.Collections .singletonList(acl.build))
574+ }
575+ }
576+ ()
577+ }
578+
514579 /**
515580 * Copies all bytes from the given input stream to the given output stream.
516581 * Neither stream is closed.
0 commit comments