3
3
4
4
package software.aws.toolkits.core.utils
5
5
6
+ import org.slf4j.Logger
6
7
import java.io.InputStream
7
8
import java.io.OutputStream
8
9
import java.nio.charset.Charset
10
+ import java.nio.file.AccessMode
9
11
import java.nio.file.FileAlreadyExistsException
10
12
import java.nio.file.Files
11
13
import java.nio.file.NoSuchFileException
12
14
import java.nio.file.Path
15
+ import java.nio.file.attribute.AclEntry
16
+ import java.nio.file.attribute.AclFileAttributeView
13
17
import java.nio.file.attribute.FileTime
14
18
import java.nio.file.attribute.PosixFilePermission
19
+ import java.nio.file.attribute.PosixFilePermissions
20
+ import java.nio.file.attribute.UserPrincipal
21
+ import kotlin.io.path.getPosixFilePermissions
22
+ import kotlin.io.path.isRegularFile
23
+
24
+ val POSIX_OWNER_ONLY_FILE = setOf (PosixFilePermission .OWNER_READ , PosixFilePermission .OWNER_WRITE )
25
+ val POSIX_OWNER_ONLY_DIR = setOf (PosixFilePermission .OWNER_READ , PosixFilePermission .OWNER_WRITE , PosixFilePermission .OWNER_EXECUTE )
15
26
16
27
fun Path.inputStream (): InputStream = Files .newInputStream(this )
17
28
fun Path.inputStreamIfExists (): InputStream ? = try {
@@ -20,26 +31,183 @@ fun Path.inputStreamIfExists(): InputStream? = try {
20
31
null
21
32
}
22
33
23
- fun Path.touch () {
24
- this .createParentDirectories()
34
+ fun Path.touch (restrictToOwner : Boolean = false) {
25
35
try {
26
- Files .createFile(this )
27
- } catch (_: FileAlreadyExistsException ) { }
36
+ if (! restrictToOwner || ! hasPosixFilePermissions()) {
37
+ Files .createFile(this )
38
+ } else {
39
+ Files .createFile(this , PosixFilePermissions .asFileAttribute(POSIX_OWNER_ONLY_FILE ))
40
+ }
41
+ } catch (_: FileAlreadyExistsException ) {}
28
42
}
29
43
30
44
fun Path.outputStream (): OutputStream {
31
45
this .createParentDirectories()
32
46
return Files .newOutputStream(this )
33
47
}
34
- fun Path.createParentDirectories () = Files .createDirectories(this .parent)
48
+
49
+ fun Path.createParentDirectories (restrictToOwner : Boolean = false) = if (! restrictToOwner || ! hasPosixFilePermissions()) {
50
+ Files .createDirectories(this .parent)
51
+ } else {
52
+ Files .createDirectories(this .parent, PosixFilePermissions .asFileAttribute(POSIX_OWNER_ONLY_DIR ))
53
+ }
54
+
35
55
fun Path.exists () = Files .exists(this )
36
56
fun Path.deleteIfExists () = Files .deleteIfExists(this )
37
57
fun Path.lastModified (): FileTime = Files .getLastModifiedTime(this )
38
58
fun Path.readText (charset : Charset = Charsets .UTF_8 ) = toFile().readText(charset)
39
59
fun Path.writeText (text : String , charset : Charset = Charsets .UTF_8 ) = toFile().writeText(text, charset)
60
+
61
+ // Comes from PosixFileAttributeView#name()
62
+ fun Path.hasPosixFilePermissions () = " posix" in this .fileSystem.supportedFileAttributeViews()
40
63
fun Path.filePermissions (permissions : Set <PosixFilePermission >) {
41
- // Comes from PosixFileAttributeView#name()
42
- if (" posix" in this .fileSystem.supportedFileAttributeViews()) {
64
+ if (hasPosixFilePermissions()) {
43
65
Files .setPosixFilePermissions(this , permissions)
44
66
}
45
67
}
68
+
69
+ fun Path.tryDirOp (log : Logger , block : Path .() -> Unit ) {
70
+ try {
71
+ log.debug { " dir op on $this " }
72
+ block(this )
73
+ } catch (e: Exception ) {
74
+ if (e !is java.nio.file.AccessDeniedException && e !is kotlin.io.AccessDeniedException ) {
75
+ throw e
76
+ }
77
+
78
+ if (! hasPosixFilePermissions()) {
79
+ throw tryAugmentExceptionMessage(e, this )
80
+ }
81
+
82
+ log.info(e) { " Attempting to handle ADE for directory operation" }
83
+ try {
84
+ var parent = if (this .isRegularFile()) { parent } else { this }
85
+
86
+ while (parent != null ) {
87
+ if (! parent.exists()) {
88
+ log.info { " ${parent.toAbsolutePath()} : does not exist yet" }
89
+ } else {
90
+ if (tryOrNull { parent.fileSystem.provider().checkAccess(parent, AccessMode .READ , AccessMode .WRITE , AccessMode .EXECUTE ) } != null ) {
91
+ log.debug { " $parent has rwx, exiting" }
92
+ // can assume parent permissions are correct
93
+ break
94
+ }
95
+
96
+ log.debug { " fixing perms for $parent " }
97
+ parent.tryFixPerms(log, POSIX_OWNER_ONLY_DIR )
98
+ }
99
+
100
+ parent = parent.parent
101
+ }
102
+ } catch (e2: Exception ) {
103
+ log.warn(e2) { " Encountered error while handling ADE for ${e.message} " }
104
+
105
+ throw tryAugmentExceptionMessage(e, this )
106
+ }
107
+
108
+ log.info { " Done attempting to handle ADE for directory operation" }
109
+ block(this )
110
+ }
111
+ }
112
+
113
+ fun <T > Path.tryFileOp (log : Logger , block : Path .() -> T ) =
114
+ try {
115
+ log.debug { " file op on $this " }
116
+ block(this )
117
+ } catch (e: Exception ) {
118
+ if (e !is java.nio.file.AccessDeniedException && e !is kotlin.io.AccessDeniedException ) {
119
+ throw e
120
+ }
121
+
122
+ if (! hasPosixFilePermissions()) {
123
+ throw tryAugmentExceptionMessage(e, this )
124
+ }
125
+
126
+ log.info(e) { " Attempting to handle ADE for file operation" }
127
+ try {
128
+ log.debug { " fixing perms for $this " }
129
+ tryFixPerms(log, POSIX_OWNER_ONLY_FILE )
130
+ } catch (e2: Exception ) {
131
+ log.warn(e2) { " Encountered error while handling ADE for ${e.message} " }
132
+
133
+ throw tryAugmentExceptionMessage(e, this )
134
+ }
135
+
136
+ log.info { " Done attempting to handle ADE for file operation" }
137
+ block(this )
138
+ }
139
+
140
+ private fun Path.tryFixPerms (log : Logger , desiredPermissions : Set <PosixFilePermission >) {
141
+ // TODO: consider handling linux ACLs
142
+ // only try ops if we own the file
143
+ // (ab)use invariant that chmod only works if you are root or the file owner
144
+ val perms = tryOrLogShortException(log) { Files .getPosixFilePermissions(this ) }
145
+ val ownership = tryOrLogShortException(log) { Files .getOwner(this ) }
146
+
147
+ log.info { " Permissions for ${toAbsolutePath()} : $ownership , $perms " }
148
+ if (perms != null && ownership != null ) {
149
+ if (ownership.name != " root" && tryOrNull { filePermissions(perms) } != null ) {
150
+ val permissions = perms + desiredPermissions
151
+ log.info { " Setting perms for ${toAbsolutePath()} : $permissions " }
152
+ filePermissions(permissions)
153
+ }
154
+ }
155
+ }
156
+
157
+ private fun tryAugmentExceptionMessage (e : Exception , path : Path ): Exception {
158
+ if (e !is java.nio.file.AccessDeniedException && e !is kotlin.io.AccessDeniedException ) {
159
+ return e
160
+ }
161
+
162
+ var potentialProblem = if (path.exists()) { path } else { path.parent }
163
+ var acls: List <AclEntry >? = null
164
+ var ownership: UserPrincipal ? = null
165
+ while (potentialProblem != null ) {
166
+ acls = tryOrNull { Files .getFileAttributeView(potentialProblem, AclFileAttributeView ::class .java).acl }
167
+ ownership = tryOrNull { Files .getOwner(potentialProblem) }
168
+
169
+ if (acls != null || ownership != null ) {
170
+ break
171
+ }
172
+
173
+ potentialProblem = potentialProblem.parent
174
+ }
175
+
176
+ val message = buildString {
177
+ // $path is automatically added to the front of the exception message
178
+ appendLine(" Exception trying to perform operation" )
179
+
180
+ if (potentialProblem != null ) {
181
+ append(" Potential issue is with $potentialProblem " )
182
+
183
+ if (ownership != null ) {
184
+ append(" , which has owner: $ownership " )
185
+ }
186
+
187
+ if (acls != null ) {
188
+ append(" , and ACL entries for: ${acls.map { it.principal() }} " )
189
+ }
190
+
191
+ val posixPermissions = tryOrNull { PosixFilePermissions .toString(potentialProblem.getPosixFilePermissions()) }
192
+ if (posixPermissions != null ) {
193
+ append(" , and POSIX permissions: $posixPermissions " )
194
+ }
195
+ }
196
+ }
197
+
198
+ return when (e) {
199
+ is kotlin.io.AccessDeniedException -> kotlin.io.AccessDeniedException (e.file, e.other, message)
200
+ is java.nio.file.AccessDeniedException -> java.nio.file.AccessDeniedException (e.file, e.otherFile, message)
201
+ // should never happen
202
+ else -> e
203
+ }.also {
204
+ it.stackTrace = e.stackTrace
205
+ }
206
+ }
207
+
208
+ private fun <T > tryOrLogShortException (log : Logger , block : () -> T ) = try {
209
+ block()
210
+ } catch (e: Exception ) {
211
+ log.warn { " ${e::class .simpleName} : ${e.message} " }
212
+ null
213
+ }
0 commit comments