@@ -16,21 +16,26 @@ import kotlinx.coroutines.launch
16
16
import kotlinx.coroutines.runBlocking
17
17
import kotlinx.coroutines.withContext
18
18
import org.apache.commons.codec.digest.DigestUtils
19
- import software.aws.toolkits.core.utils.outputStream
20
- import software.aws.toolkits.core.utils.putNextEntry
21
- import software.aws.toolkits.jetbrains.core.coroutines.EDT
19
+ import org.apache.commons.io.FileUtils
22
20
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
23
21
import software.aws.toolkits.jetbrains.services.telemetry.ALLOWED_CODE_EXTENSIONS
24
22
import software.aws.toolkits.resources.AwsCoreBundle
25
23
import software.aws.toolkits.telemetry.AmazonqTelemetry
26
24
import java.io.File
27
25
import java.io.FileInputStream
26
+ import java.net.URI
27
+ import java.nio.file.FileSystem
28
+ import java.nio.file.FileSystems
28
29
import java.nio.file.Files
29
30
import java.nio.file.Path
31
+ import java.nio.file.Paths
32
+ import java.nio.file.StandardCopyOption
30
33
import java.util.Base64
31
- import java.util.zip.ZipOutputStream
34
+ import java.util.UUID
32
35
import kotlin.coroutines.coroutineContext
33
36
import kotlin.io.path.Path
37
+ import kotlin.io.path.createParentDirectories
38
+ import kotlin.io.path.getPosixFilePermissions
34
39
import kotlin.io.path.relativeTo
35
40
36
41
interface RepoSizeError {
@@ -41,41 +46,45 @@ class RepoSizeLimitError(override val message: String) : RuntimeException(), Rep
41
46
class FeatureDevSessionContext (val project : Project , val maxProjectSizeBytes : Long? = null ) {
42
47
// TODO: Need to correct this class location in the modules going further to support both amazonq and codescan.
43
48
44
- private val ignorePatterns = setOf (
45
- " \\ .aws-sam" ,
46
- " \\ .svn" ,
47
- " \\ .hg/?" ,
48
- " \\ .rvm" ,
49
- " \\ .git/?" ,
50
- " \\ .gitignore" ,
51
- " \\ .project" ,
52
- " \\ .gem" ,
53
- " /\\ .idea/?" ,
54
- " \\ .zip$" ,
55
- " \\ .bin$" ,
56
- " \\ .png$" ,
57
- " \\ .jpg$" ,
58
- " \\ .svg$" ,
59
- " \\ .pyc$" ,
60
- " /license\\ .txt$" ,
61
- " /License\\ .txt$" ,
62
- " /LICENSE\\ .txt$" ,
63
- " /license\\ .md$" ,
64
- " /License\\ .md$" ,
65
- " /LICENSE\\ .md$" ,
66
- " node_modules/?" ,
67
- " build/?" ,
68
- " dist/?"
69
- ).map { Regex (it) }
49
+ private val additionalGitIgnoreRules = setOf (
50
+ " .aws-sam" ,
51
+ " .gem" ,
52
+ " .git" ,
53
+ " .gitignore" ,
54
+ " .gradle" ,
55
+ " .hg" ,
56
+ " .idea" ,
57
+ " .project" ,
58
+ " .rvm" ,
59
+ " .svn" ,
60
+ " *.zip" ,
61
+ " *.bin" ,
62
+ " *.png" ,
63
+ " *.jpg" ,
64
+ " *.svg" ,
65
+ " *.pyc" ,
66
+ " license.txt" ,
67
+ " License.txt" ,
68
+ " LICENSE.txt" ,
69
+ " license.md" ,
70
+ " License.md" ,
71
+ " LICENSE.md" ,
72
+ " node_modules" ,
73
+ " build" ,
74
+ " dist"
75
+ )
70
76
71
77
// well known source files that do not have extensions
72
78
private val wellKnownSourceFiles = setOf (
73
79
" Dockerfile" ,
74
- " Dockerfile.build"
80
+ " Dockerfile.build" ,
81
+ " gradlew" ,
82
+ " mvnw"
75
83
)
76
84
77
85
// projectRoot: is the directory where the project is located when selected to open a project.
78
86
val projectRoot = project.guessProjectDir() ? : error(" Cannot guess base directory for project ${project.name} " )
87
+ private val projectRootPath = Paths .get(projectRoot.path) ? : error(" Can not find project root path" )
79
88
80
89
// selectedSourceFolder": is the directory selected in replacement of the root, this happens when the project is too big to bundle for uploading.
81
90
private var _selectedSourceFolder = projectRoot
@@ -85,10 +94,10 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
85
94
init {
86
95
ignorePatternsWithGitIgnore = try {
87
96
buildList {
88
- addAll(ignorePatterns )
89
- parseGitIgnore().mapNotNull { pattern ->
90
- runCatching { Regex ( pattern) }.getOrNull()
91
- }. let { addAll(it ) }
97
+ addAll(additionalGitIgnoreRules.map { convertGitIgnorePatternToRegex(it) } )
98
+ addAll( parseGitIgnore())
99
+ }.mapNotNull { pattern ->
100
+ runCatching { Regex (pattern ) }.getOrNull()
92
101
}
93
102
} catch (e: Exception ) {
94
103
emptyList()
@@ -108,21 +117,26 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
108
117
fun isFileExtensionAllowed (file : VirtualFile ): Boolean {
109
118
// if it is a directory, it is allowed
110
119
if (file.isDirectory) return true
111
-
112
120
val extension = file.extension ? : return false
113
121
return ALLOWED_CODE_EXTENSIONS .contains(extension)
114
122
}
115
123
116
124
private fun ignoreFileByExtension (file : VirtualFile ) =
117
125
! isFileExtensionAllowed(file)
118
126
119
- suspend fun ignoreFile (file : VirtualFile ): Boolean = ignoreFile(file.path )
127
+ suspend fun ignoreFile (file : VirtualFile ): Boolean = ignoreFile(file.presentableUrl )
120
128
121
129
suspend fun ignoreFile (path : String ): Boolean {
122
130
// this method reads like something a JS dev would write and doesn't do what the author thinks
123
131
val deferredResults = ignorePatternsWithGitIgnore.map { pattern ->
124
132
withContext(coroutineContext) {
125
- async { pattern.containsMatchIn(path) }
133
+ // avoid partial match (pattern.containsMatchIn) since it causes us matching files
134
+ // against folder patterns. (e.g. settings.gradle ignored by .gradle rule!)
135
+ // we convert the glob rules to regex, add a trailing /* to all rules and then match
136
+ // entries against them by adding a trailing /.
137
+ // TODO: Add unit tests for gitignore matching
138
+ val relative = if (path.startsWith(projectRootPath.toString())) Paths .get(path).relativeTo(projectRootPath) else path
139
+ async { pattern.matches(" $relative /" ) }
126
140
}
127
141
}
128
142
@@ -132,7 +146,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
132
146
return deferredResults.any { it.await() }
133
147
}
134
148
135
- private fun wellKnown (file : VirtualFile ): Boolean = wellKnownSourceFiles.contains(file.name)
149
+ fun wellKnown (file : VirtualFile ): Boolean = wellKnownSourceFiles.contains(file.name)
136
150
137
151
suspend fun zipFiles (projectRoot : VirtualFile ): File = withContext(getCoroutineBgContext()) {
138
152
val files = mutableListOf<VirtualFile >()
@@ -187,22 +201,43 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
187
201
}
188
202
}
189
203
190
- createTemporaryZipFileAsync { zipOutput ->
204
+ val zipFilePath = createTemporaryZipFileAsync { zipfs ->
205
+ val posixFileAttributeSubstr = " posix"
206
+ val isPosix = FileSystems .getDefault().supportedFileAttributeViews().contains(posixFileAttributeSubstr)
191
207
filesToIncludeFlow.collect { file ->
192
- try {
193
- val relativePath = Path (file.path).relativeTo(projectRoot.toNioPath())
194
- zipOutput.putNextEntry(relativePath.toString(), Path (file.path))
195
- } catch (e: NoSuchFileException ) {
196
- // Noop: Skip if file was deleted
208
+
209
+ if (! file.isDirectory) {
210
+ val externalFilePath = Path (file.path)
211
+ val relativePath = Path (file.path).relativeTo(projectRootPath)
212
+ val zipfsPath = zipfs.getPath(" /$relativePath " )
213
+ withContext(getCoroutineBgContext()) {
214
+ zipfsPath.createParentDirectories()
215
+ try {
216
+ Files .copy(externalFilePath, zipfsPath, StandardCopyOption .REPLACE_EXISTING )
217
+ if (isPosix) {
218
+ val zipPermissionAttributeName = " zip:permissions"
219
+ Files .setAttribute(zipfsPath, zipPermissionAttributeName, externalFilePath.getPosixFilePermissions())
220
+ }
221
+ } catch (e: NoSuchFileException ) {
222
+ // Noop: Skip if file was deleted
223
+ }
224
+ }
197
225
}
198
226
}
199
227
}
228
+ zipFilePath
200
229
}.toFile()
201
230
202
- private suspend fun createTemporaryZipFileAsync (block : suspend (ZipOutputStream ) -> Unit ): Path = withContext(EDT ) {
203
- val file = Files .createTempFile(null , " .zip" )
204
- ZipOutputStream (file.outputStream()).use { zipOutput -> block(zipOutput) }
205
- file
231
+ private suspend fun createTemporaryZipFileAsync (block : suspend (FileSystem ) -> Unit ): Path = withContext(getCoroutineBgContext()) {
232
+ // Don't use Files.createTempFile since the file must not be created for ZipFS to work
233
+ val tempFilePath: Path = Paths .get(FileUtils .getTempDirectory().absolutePath, " ${UUID .randomUUID()} .zip" )
234
+ val uri = URI .create(" jar:${tempFilePath.toUri()} " )
235
+ val env = hashMapOf(" create" to " true" )
236
+ val zipfs = FileSystems .newFileSystem(uri, env)
237
+ zipfs.use {
238
+ block(zipfs)
239
+ }
240
+ tempFilePath
206
241
}
207
242
208
243
private fun parseGitIgnore (): Set <String > {
@@ -220,7 +255,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
220
255
private fun convertGitIgnorePatternToRegex (pattern : String ): String = pattern
221
256
.replace(" ." , " \\ ." )
222
257
.replace(" *" , " .*" )
223
- .let { if (it.endsWith(" /" )) " $it ? " else it } // Handle directory-specific patterns by optionally matching trailing slash
258
+ .let { if (it.endsWith(" /" )) " $it .* " else " $it /.* " } // Add a trailing /* to all patterns. (we add a trailing / to all files when matching)
224
259
225
260
var selectedSourceFolder: VirtualFile
226
261
set(newRoot) {
0 commit comments