@@ -16,22 +16,27 @@ import kotlinx.coroutines.launch
1616import kotlinx.coroutines.runBlocking
1717import kotlinx.coroutines.withContext
1818import 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
2220import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
2321import software.aws.toolkits.jetbrains.services.telemetry.ALLOWED_CODE_EXTENSIONS
2422import software.aws.toolkits.jetbrains.utils.isDevFile
2523import software.aws.toolkits.resources.AwsCoreBundle
2624import software.aws.toolkits.telemetry.AmazonqTelemetry
2725import java.io.File
2826import java.io.FileInputStream
27+ import java.net.URI
28+ import java.nio.file.FileSystem
29+ import java.nio.file.FileSystems
2930import java.nio.file.Files
3031import java.nio.file.Path
32+ import java.nio.file.Paths
33+ import java.nio.file.StandardCopyOption
3134import java.util.Base64
32- import java.util.zip.ZipOutputStream
35+ import java.util.UUID
3336import kotlin.coroutines.coroutineContext
3437import kotlin.io.path.Path
38+ import kotlin.io.path.createParentDirectories
39+ import kotlin.io.path.getPosixFilePermissions
3540import kotlin.io.path.relativeTo
3641
3742interface RepoSizeError {
@@ -42,40 +47,45 @@ class RepoSizeLimitError(override val message: String) : RuntimeException(), Rep
4247class FeatureDevSessionContext (val project : Project , val maxProjectSizeBytes : Long? = null ) {
4348 // TODO: Need to correct this class location in the modules going further to support both amazonq and codescan.
4449
45- private val ignorePatterns = setOf (
46- " \\ .aws-sam" ,
47- " \\ .svn" ,
48- " \\ .hg/?" ,
49- " \\ .rvm" ,
50- " \\ .git/?" ,
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) }
50+ private val additionalGitIgnoreRules = setOf (
51+ " .aws-sam" ,
52+ " .gem" ,
53+ " .git" ,
54+ " .gitignore" ,
55+ " .gradle" ,
56+ " .hg" ,
57+ " .idea" ,
58+ " .project" ,
59+ " .rvm" ,
60+ " .svn" ,
61+ " *.zip" ,
62+ " *.bin" ,
63+ " *.png" ,
64+ " *.jpg" ,
65+ " *.svg" ,
66+ " *.pyc" ,
67+ " license.txt" ,
68+ " License.txt" ,
69+ " LICENSE.txt" ,
70+ " license.md" ,
71+ " License.md" ,
72+ " LICENSE.md" ,
73+ " node_modules" ,
74+ " build" ,
75+ " dist"
76+ )
7077
7178 // well known source files that do not have extensions
7279 private val wellKnownSourceFiles = setOf (
7380 " Dockerfile" ,
74- " Dockerfile.build"
81+ " Dockerfile.build" ,
82+ " gradlew" ,
83+ " mvnw"
7584 )
7685
7786 // projectRoot: is the directory where the project is located when selected to open a project.
7887 val projectRoot = project.guessProjectDir() ? : error(" Cannot guess base directory for project ${project.name} " )
88+ private val projectRootPath = Paths .get(projectRoot.path) ? : error(" Can not find project root path" )
7989
8090 // selectedSourceFolder: is the directory selected in replacement of the root, this happens when the project is too big to bundle for uploading.
8191 private var _selectedSourceFolder = projectRoot
@@ -85,10 +95,10 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
8595 init {
8696 ignorePatternsWithGitIgnore = try {
8797 buildList {
88- addAll(ignorePatterns )
89- parseGitIgnore().mapNotNull { pattern ->
90- runCatching { Regex ( pattern) }.getOrNull()
91- }. let { addAll(it ) }
98+ addAll(additionalGitIgnoreRules.map { convertGitIgnorePatternToRegex(it) } )
99+ addAll( parseGitIgnore())
100+ }.mapNotNull { pattern ->
101+ runCatching { Regex (pattern ) }.getOrNull()
92102 }
93103 } catch (e: Exception ) {
94104 emptyList()
@@ -116,21 +126,26 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
116126 fun isFileExtensionAllowed (file : VirtualFile ): Boolean {
117127 // if it is a directory, it is allowed
118128 if (file.isDirectory) return true
119-
120129 val extension = file.extension ? : return false
121130 return ALLOWED_CODE_EXTENSIONS .contains(extension)
122131 }
123132
124133 private fun ignoreFileByExtension (file : VirtualFile ) =
125134 ! isFileExtensionAllowed(file)
126135
127- suspend fun ignoreFile (file : VirtualFile ): Boolean = ignoreFile(file.path )
136+ suspend fun ignoreFile (file : VirtualFile ): Boolean = ignoreFile(file.presentableUrl )
128137
129138 suspend fun ignoreFile (path : String ): Boolean {
130139 // this method reads like something a JS dev would write and doesn't do what the author thinks
131140 val deferredResults = ignorePatternsWithGitIgnore.map { pattern ->
132141 withContext(coroutineContext) {
133- async { pattern.containsMatchIn(path) }
142+ // avoid partial match (pattern.containsMatchIn) since it causes us matching files
143+ // against folder patterns. (e.g. settings.gradle ignored by .gradle rule!)
144+ // we convert the glob rules to regex, add a trailing /* to all rules and then match
145+ // entries against them by adding a trailing /.
146+ // TODO: Add unit tests for gitignore matching
147+ val relative = if (path.startsWith(projectRootPath.toString())) Paths .get(path).relativeTo(projectRootPath) else path
148+ async { pattern.matches(" $relative /" ) }
134149 }
135150 }
136151
@@ -201,22 +216,43 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
201216 }
202217 }
203218
204- createTemporaryZipFileAsync { zipOutput ->
219+ val zipFilePath = createTemporaryZipFileAsync { zipfs ->
220+ val posixFileAttributeSubstr = " posix"
221+ val isPosix = FileSystems .getDefault().supportedFileAttributeViews().contains(posixFileAttributeSubstr)
205222 filesToIncludeFlow.collect { file ->
206- try {
207- val relativePath = Path (file.path).relativeTo(projectRoot.toNioPath())
208- zipOutput.putNextEntry(relativePath.toString(), Path (file.path))
209- } catch (e: NoSuchFileException ) {
210- // Noop: Skip if file was deleted
223+
224+ if (! file.isDirectory) {
225+ val externalFilePath = Path (file.path)
226+ val relativePath = Path (file.path).relativeTo(projectRootPath)
227+ val zipfsPath = zipfs.getPath(" /$relativePath " )
228+ withContext(getCoroutineBgContext()) {
229+ zipfsPath.createParentDirectories()
230+ try {
231+ Files .copy(externalFilePath, zipfsPath, StandardCopyOption .REPLACE_EXISTING )
232+ if (isPosix) {
233+ val zipPermissionAttributeName = " zip:permissions"
234+ Files .setAttribute(zipfsPath, zipPermissionAttributeName, externalFilePath.getPosixFilePermissions())
235+ }
236+ } catch (e: NoSuchFileException ) {
237+ // Noop: Skip if file was deleted
238+ }
239+ }
211240 }
212241 }
213242 }
243+ zipFilePath
214244 }.toFile()
215245
216- private suspend fun createTemporaryZipFileAsync (block : suspend (ZipOutputStream ) -> Unit ): Path = withContext(EDT ) {
217- val file = Files .createTempFile(null , " .zip" )
218- ZipOutputStream (file.outputStream()).use { zipOutput -> block(zipOutput) }
219- file
246+ private suspend fun createTemporaryZipFileAsync (block : suspend (FileSystem ) -> Unit ): Path = withContext(getCoroutineBgContext()) {
247+ // Don't use Files.createTempFile since the file must not be created for ZipFS to work
248+ val tempFilePath: Path = Paths .get(FileUtils .getTempDirectory().absolutePath, " ${UUID .randomUUID()} .zip" )
249+ val uri = URI .create(" jar:${tempFilePath.toUri()} " )
250+ val env = hashMapOf(" create" to " true" )
251+ val zipfs = FileSystems .newFileSystem(uri, env)
252+ zipfs.use {
253+ block(zipfs)
254+ }
255+ tempFilePath
220256 }
221257
222258 private fun parseGitIgnore (): Set <String > {
@@ -234,7 +270,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
234270 private fun convertGitIgnorePatternToRegex (pattern : String ): String = pattern
235271 .replace(" ." , " \\ ." )
236272 .replace(" *" , " .*" )
237- .let { if (it.endsWith(" /" )) " $it ? " else it } // Handle directory-specific patterns by optionally matching trailing slash
273+ .let { if (it.endsWith(" /" )) " $it .* " else " $it /.* " } // Add a trailing /* to all patterns. (we add a trailing / to all files when matching)
238274
239275 var selectedSourceFolder: VirtualFile
240276 set(newRoot) {
0 commit comments