@@ -16,21 +16,26 @@ 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.resources.AwsCoreBundle
2523import software.aws.toolkits.telemetry.AmazonqTelemetry
2624import java.io.File
2725import java.io.FileInputStream
26+ import java.net.URI
27+ import java.nio.file.FileSystem
28+ import java.nio.file.FileSystems
2829import java.nio.file.Files
2930import java.nio.file.Path
31+ import java.nio.file.Paths
32+ import java.nio.file.StandardCopyOption
3033import java.util.Base64
31- import java.util.zip.ZipOutputStream
34+ import java.util.UUID
3235import kotlin.coroutines.coroutineContext
3336import kotlin.io.path.Path
37+ import kotlin.io.path.createParentDirectories
38+ import kotlin.io.path.getPosixFilePermissions
3439import kotlin.io.path.relativeTo
3540
3641interface RepoSizeError {
@@ -41,41 +46,45 @@ class RepoSizeLimitError(override val message: String) : RuntimeException(), Rep
4146class FeatureDevSessionContext (val project : Project , val maxProjectSizeBytes : Long? = null ) {
4247 // TODO: Need to correct this class location in the modules going further to support both amazonq and codescan.
4348
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+ )
7076
7177 // well known source files that do not have extensions
7278 private val wellKnownSourceFiles = setOf (
7379 " Dockerfile" ,
74- " Dockerfile.build"
80+ " Dockerfile.build" ,
81+ " gradlew" ,
82+ " mvnw"
7583 )
7684
7785 // projectRoot: is the directory where the project is located when selected to open a project.
7886 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" )
7988
8089 // selectedSourceFolder": is the directory selected in replacement of the root, this happens when the project is too big to bundle for uploading.
8190 private var _selectedSourceFolder = projectRoot
@@ -85,10 +94,10 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
8594 init {
8695 ignorePatternsWithGitIgnore = try {
8796 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()
92101 }
93102 } catch (e: Exception ) {
94103 emptyList()
@@ -108,21 +117,26 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
108117 fun isFileExtensionAllowed (file : VirtualFile ): Boolean {
109118 // if it is a directory, it is allowed
110119 if (file.isDirectory) return true
111-
112120 val extension = file.extension ? : return false
113121 return ALLOWED_CODE_EXTENSIONS .contains(extension)
114122 }
115123
116124 private fun ignoreFileByExtension (file : VirtualFile ) =
117125 ! isFileExtensionAllowed(file)
118126
119- suspend fun ignoreFile (file : VirtualFile ): Boolean = ignoreFile(file.path )
127+ suspend fun ignoreFile (file : VirtualFile ): Boolean = ignoreFile(file.presentableUrl )
120128
121129 suspend fun ignoreFile (path : String ): Boolean {
122130 // this method reads like something a JS dev would write and doesn't do what the author thinks
123131 val deferredResults = ignorePatternsWithGitIgnore.map { pattern ->
124132 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 /" ) }
126140 }
127141 }
128142
@@ -132,7 +146,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
132146 return deferredResults.any { it.await() }
133147 }
134148
135- private fun wellKnown (file : VirtualFile ): Boolean = wellKnownSourceFiles.contains(file.name)
149+ fun wellKnown (file : VirtualFile ): Boolean = wellKnownSourceFiles.contains(file.name)
136150
137151 suspend fun zipFiles (projectRoot : VirtualFile ): File = withContext(getCoroutineBgContext()) {
138152 val files = mutableListOf<VirtualFile >()
@@ -187,22 +201,43 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
187201 }
188202 }
189203
190- createTemporaryZipFileAsync { zipOutput ->
204+ val zipFilePath = createTemporaryZipFileAsync { zipfs ->
205+ val posixFileAttributeSubstr = " posix"
206+ val isPosix = FileSystems .getDefault().supportedFileAttributeViews().contains(posixFileAttributeSubstr)
191207 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+ }
197225 }
198226 }
199227 }
228+ zipFilePath
200229 }.toFile()
201230
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
206241 }
207242
208243 private fun parseGitIgnore (): Set <String > {
@@ -220,7 +255,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
220255 private fun convertGitIgnorePatternToRegex (pattern : String ): String = pattern
221256 .replace(" ." , " \\ ." )
222257 .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)
224259
225260 var selectedSourceFolder: VirtualFile
226261 set(newRoot) {
0 commit comments