@@ -10,27 +10,34 @@ import com.intellij.openapi.vfs.VirtualFile
1010import com.intellij.openapi.vfs.VirtualFileVisitor
1111import com.intellij.openapi.vfs.isFile
1212import com.intellij.platform.ide.progress.withBackgroundProgress
13+ import kotlinx.coroutines.Dispatchers
1314import kotlinx.coroutines.async
1415import kotlinx.coroutines.flow.channelFlow
1516import kotlinx.coroutines.launch
1617import kotlinx.coroutines.runBlocking
1718import kotlinx.coroutines.withContext
1819import org.apache.commons.codec.digest.DigestUtils
19- import software.aws.toolkits.core.utils.outputStream
20- import software.aws.toolkits.core.utils.putNextEntry
20+ import org.apache.commons.io.FileUtils
2121import software.aws.toolkits.jetbrains.core.coroutines.EDT
2222import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
2323import software.aws.toolkits.jetbrains.services.telemetry.ALLOWED_CODE_EXTENSIONS
2424import software.aws.toolkits.resources.AwsCoreBundle
2525import software.aws.toolkits.telemetry.AmazonqTelemetry
2626import java.io.File
2727import java.io.FileInputStream
28+ import java.net.URI
29+ import java.nio.file.FileSystem
30+ import java.nio.file.FileSystems
2831import java.nio.file.Files
2932import java.nio.file.Path
33+ import java.nio.file.Paths
34+ import java.nio.file.StandardCopyOption
3035import java.util.Base64
31- import java.util.zip.ZipOutputStream
36+ import java.util.UUID
3237import kotlin.coroutines.coroutineContext
3338import kotlin.io.path.Path
39+ import kotlin.io.path.createParentDirectories
40+ import kotlin.io.path.getPosixFilePermissions
3441import kotlin.io.path.relativeTo
3542
3643interface RepoSizeError {
@@ -74,6 +81,12 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
7481 " Dockerfile.build"
7582 )
7683
84+ // patterns to explicitly allow unless matched with gitignore rules
85+ private val allowedPatterns = setOf (
86+ " .*mvn.*" ,
87+ " .*gradle.*" ,
88+ ).map { Regex (it) }
89+
7790 // projectRoot: is the directory where the project is located when selected to open a project.
7891 val projectRoot = project.guessProjectDir() ? : error(" Cannot guess base directory for project ${project.name} " )
7992
@@ -108,7 +121,10 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
108121 fun isFileExtensionAllowed (file : VirtualFile ): Boolean {
109122 // if it is a directory, it is allowed
110123 if (file.isDirectory) return true
111-
124+ val explicitAllowed = allowedPatterns.map { pattern -> pattern.matches(file.path) }.any { it }
125+ if (explicitAllowed) {
126+ return true
127+ }
112128 val extension = file.extension ? : return false
113129 return ALLOWED_CODE_EXTENSIONS .contains(extension)
114130 }
@@ -122,7 +138,12 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
122138 // this method reads like something a JS dev would write and doesn't do what the author thinks
123139 val deferredResults = ignorePatternsWithGitIgnore.map { pattern ->
124140 withContext(coroutineContext) {
125- async { pattern.containsMatchIn(path) }
141+ // avoid partial match (pattern.containsMatchIn) since it causes us matching files
142+ // against folder patterns. (e.g. settings.gradle ignored by .gradle rule!)
143+ // we convert the glob rules to regex, add a trailing /* to all rules and then match
144+ // entries against them by adding a trailing /.
145+ // TODO: Add unit tests for gitignore matching
146+ async { pattern.matches(" $path /" ) }
126147 }
127148 }
128149
@@ -187,18 +208,34 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
187208 }
188209 }
189210
190- createTemporaryZipFileAsync { zipOutput ->
211+ val zipFilePath = createTemporaryZipFileAsync { zipfs ->
191212 filesToIncludeFlow.collect { file ->
192- val relativePath = Path (file.path).relativeTo(projectRoot.toNioPath())
193- zipOutput.putNextEntry(relativePath.toString(), Path (file.path))
213+ if (! file.isDirectory) {
214+ val externalFilePath = Path (file.path)
215+ val externalFilePermissions = externalFilePath.getPosixFilePermissions()
216+ val relativePath = Path (file.path).relativeTo(projectRoot.toNioPath())
217+ val zipfsPath = zipfs.getPath(" /${relativePath} " )
218+ withContext(Dispatchers .IO ) {
219+ zipfsPath.createParentDirectories()
220+ Files .copy(externalFilePath, zipfsPath, StandardCopyOption .REPLACE_EXISTING )
221+ Files .setAttribute(zipfsPath, " zip:permissions" , externalFilePermissions);
222+ }
223+ }
194224 }
195225 }
226+ zipFilePath
196227 }.toFile()
197228
198- private suspend fun createTemporaryZipFileAsync (block : suspend (ZipOutputStream ) -> Unit ): Path = withContext(EDT ) {
199- val file = Files .createTempFile(null , " .zip" )
200- ZipOutputStream (file.outputStream()).use { zipOutput -> block(zipOutput) }
201- file
229+ private suspend fun createTemporaryZipFileAsync (block : suspend (FileSystem ) -> Unit ): Path = withContext(EDT ) {
230+ // Don't use Files.createTempFile since the file must not be created for ZipFS to work
231+ val tempFilePath: Path = Paths .get(FileUtils .getTempDirectory().getAbsolutePath(), " ${UUID .randomUUID()} .zip" )
232+ val uri = URI .create(" jar:file:${tempFilePath} " )
233+ val env = hashMapOf(" create" to " true" )
234+ val zipfs = FileSystems .newFileSystem(uri, env)
235+ zipfs.use {
236+ block(zipfs)
237+ }
238+ tempFilePath
202239 }
203240
204241 private fun parseGitIgnore (): Set <String > {
@@ -216,7 +253,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
216253 private fun convertGitIgnorePatternToRegex (pattern : String ): String = pattern
217254 .replace(" ." , " \\ ." )
218255 .replace(" *" , " .*" )
219- .let { if (it.endsWith(" /" )) " $it ? " else it } // Handle directory-specific patterns by optionally matching trailing slash
256+ .let { if (it.endsWith(" /" )) " $it .* " else " $it /.* " } // Add a trailing /* to all patterns. (we add a trailing / to all files when matching)
220257
221258 var selectedSourceFolder: VirtualFile
222259 set(newRoot) {
0 commit comments