Skip to content

Commit bd4dd63

Browse files
feat(amazonq): Extract ZIP File and Unit Test Cases (#5416)
* Adding unit test cases * Added extractZipFile functionality
1 parent 35b0424 commit bd4dd63

File tree

8 files changed

+443
-61
lines changed

8 files changed

+443
-61
lines changed

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts
55

66
import com.intellij.util.io.createDirectories
77
import com.intellij.util.text.SemVer
8+
import org.jetbrains.annotations.VisibleForTesting
89
import software.aws.toolkits.core.utils.deleteIfExists
910
import software.aws.toolkits.core.utils.error
1011
import software.aws.toolkits.core.utils.exists
@@ -45,9 +46,23 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH,
4546
}
4647

4748
fun deleteOlderLspArtifacts(manifestVersionRanges: ArtifactManager.SupportedManifestVersionRange) {
49+
val validVersions = getAllLocalLspArtifactsWithinManifestRange(manifestVersionRanges)
50+
51+
// Keep the latest 2 versions, delete others
52+
validVersions.drop(2).forEach { (folder, _) ->
53+
try {
54+
folder.toFile().deleteRecursively()
55+
logger.info { "Deleted older LSP artifact: ${folder.fileName}" }
56+
} catch (e: Exception) {
57+
logger.error(e) { "Failed to delete older LSP artifact: ${folder.fileName}" }
58+
}
59+
}
60+
}
61+
62+
fun getAllLocalLspArtifactsWithinManifestRange(manifestVersionRanges: ArtifactManager.SupportedManifestVersionRange): List<Pair<Path, SemVer>> {
4863
val localFolders = getSubFolders(lspArtifactsPath)
4964

50-
val validVersions = localFolders
65+
return localFolders
5166
.mapNotNull { localFolder ->
5267
SemVer.parseFromText(localFolder.fileName.toString())?.let { semVer ->
5368
if (semVer in manifestVersionRanges.startVersion..manifestVersionRanges.endVersion) {
@@ -58,16 +73,6 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH,
5873
}
5974
}
6075
.sortedByDescending { (_, semVer) -> semVer }
61-
62-
// Keep the latest 2 versions, delete others
63-
validVersions.drop(2).forEach { (folder, _) ->
64-
try {
65-
folder.toFile().deleteRecursively()
66-
logger.info { "Deleted older LSP artifact: ${folder.fileName}" }
67-
} catch (e: Exception) {
68-
logger.error(e) { "Failed to delete older LSP artifact: ${folder.fileName}" }
69-
}
70-
}
7176
}
7277

7378
fun getExistingLspArtifacts(versions: List<ManifestManager.Version>, target: ManifestManager.VersionTarget?): Boolean {
@@ -103,23 +108,27 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH,
103108
logger.info { "Attempt ${currentAttempt.get()} of $maxDownloadAttempts to download LSP artifacts" }
104109

105110
try {
106-
if (downloadLspArtifacts(temporaryDownloadPath, target)) {
111+
if (downloadLspArtifacts(temporaryDownloadPath, target) && target != null && !target.contents.isNullOrEmpty()) {
107112
moveFilesFromSourceToDestination(temporaryDownloadPath, downloadPath)
113+
target.contents
114+
.mapNotNull { it.filename }
115+
.forEach { filename -> extractZipFile(downloadPath.resolve(filename), downloadPath) }
108116
logger.info { "Successfully downloaded and moved LSP artifacts to $downloadPath" }
109117
return
110118
}
111119
} catch (e: Exception) {
112120
logger.error(e) { "Failed to download/move LSP artifacts on attempt ${currentAttempt.get()}" }
113121
temporaryDownloadPath.toFile().deleteRecursively()
114-
115-
if (currentAttempt.get() >= maxDownloadAttempts) {
116-
throw LspException("Failed to download LSP artifacts after $maxDownloadAttempts attempts", LspException.ErrorCode.DOWNLOAD_FAILED)
117-
}
122+
downloadPath.toFile().deleteRecursively()
118123
}
119124
}
125+
if (currentAttempt.get() >= maxDownloadAttempts) {
126+
throw LspException("Failed to download LSP artifacts after $maxDownloadAttempts attempts", LspException.ErrorCode.DOWNLOAD_FAILED)
127+
}
120128
}
121129

122-
private fun downloadLspArtifacts(downloadPath: Path, target: ManifestManager.VersionTarget?): Boolean {
130+
@VisibleForTesting
131+
internal fun downloadLspArtifacts(downloadPath: Path, target: ManifestManager.VersionTarget?): Boolean {
123132
if (target == null || target.contents.isNullOrEmpty()) {
124133
logger.warn { "No target contents available for download" }
125134
return false
@@ -166,7 +175,8 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH,
166175
}
167176
}
168177

169-
private fun validateFileHash(filePath: Path, expectedHash: String?): Boolean {
178+
@VisibleForTesting
179+
internal fun validateFileHash(filePath: Path, expectedHash: String?): Boolean {
170180
if (expectedHash == null) return false
171181
val contentHash = generateSHA384Hash(filePath)
172182
return "sha384:$contentHash" == expectedHash

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactManager.kt

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@
44
package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts
55

66
import com.intellij.util.text.SemVer
7-
import org.assertj.core.util.VisibleForTesting
7+
import org.jetbrains.annotations.VisibleForTesting
88
import software.aws.toolkits.core.utils.error
99
import software.aws.toolkits.core.utils.getLogger
1010
import software.aws.toolkits.core.utils.info
1111
import software.aws.toolkits.jetbrains.services.amazonq.project.manifest.ManifestManager
1212

13-
class ArtifactManager {
13+
class ArtifactManager(
14+
private val manifestFetcher: ManifestFetcher = ManifestFetcher(),
15+
private val artifactHelper: ArtifactHelper = ArtifactHelper(),
16+
manifestRange: SupportedManifestVersionRange?,
17+
) {
1418

1519
data class SupportedManifestVersionRange(
1620
val startVersion: SemVer,
@@ -21,20 +25,7 @@ class ArtifactManager {
2125
val inRangeVersions: List<ManifestManager.Version>,
2226
)
2327

24-
private val manifestFetcher: ManifestFetcher
25-
private val artifactHelper: ArtifactHelper
26-
private val manifestVersionRanges: SupportedManifestVersionRange
27-
28-
// Primary constructor with config
29-
constructor(
30-
manifestFetcher: ManifestFetcher = ManifestFetcher(),
31-
artifactFetcher: ArtifactHelper = ArtifactHelper(),
32-
manifestRange: SupportedManifestVersionRange?,
33-
) {
34-
manifestVersionRanges = manifestRange ?: DEFAULT_VERSION_RANGE
35-
this.manifestFetcher = manifestFetcher
36-
this.artifactHelper = artifactFetcher
37-
}
28+
private val manifestVersionRanges: SupportedManifestVersionRange = manifestRange ?: DEFAULT_VERSION_RANGE
3829

3930
// Secondary constructor with no parameters
4031
constructor() : this(ManifestFetcher(), ArtifactHelper(), null)
@@ -57,7 +48,11 @@ class ArtifactManager {
5748
this.artifactHelper.removeDelistedVersions(lspVersions.deListedVersions)
5849

5950
if (lspVersions.inRangeVersions.isEmpty()) {
60-
// No versions are found which are in the given range.
51+
// No versions are found which are in the given range. Fallback to local lsp artifacts.
52+
val localLspArtifacts = this.artifactHelper.getAllLocalLspArtifactsWithinManifestRange(manifestVersionRanges)
53+
if (localLspArtifacts.isNotEmpty()) {
54+
return
55+
}
6156
throw LspException("Language server versions not found in manifest.", LspException.ErrorCode.NO_COMPATIBLE_LSP_VERSION)
6257
}
6358

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspException.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class LspException(message: String, private val errorCode: ErrorCode, cause: Thr
1111
HASH_MISMATCH,
1212
TARGET_NOT_FOUND,
1313
NO_COMPATIBLE_LSP_VERSION,
14+
UNZIP_FAILED,
1415
}
1516

1617
override fun toString(): String = buildString {

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtils.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@ import com.intellij.openapi.util.SystemInfo
77
import com.intellij.openapi.util.text.StringUtil
88
import com.intellij.util.io.DigestUtil
99
import com.intellij.util.system.CpuArch
10+
import software.aws.toolkits.core.utils.createParentDirectories
11+
import software.aws.toolkits.core.utils.exists
12+
import java.io.FileNotFoundException
13+
import java.io.FileOutputStream
1014
import java.nio.file.Files
1115
import java.nio.file.Path
1216
import java.nio.file.Paths
1317
import java.nio.file.StandardCopyOption
1418
import java.security.MessageDigest
19+
import java.util.zip.ZipFile
1520
import kotlin.io.path.isDirectory
1621
import kotlin.io.path.listDirectoryEntries
1722

@@ -66,3 +71,26 @@ fun moveFilesFromSourceToDestination(sourceDir: Path, targetDir: Path) {
6671
throw IllegalStateException("Failed to move files from $sourceDir to $targetDir", e)
6772
}
6873
}
74+
75+
fun extractZipFile(zipFilePath: Path, destDir: Path) {
76+
if (!zipFilePath.exists()) {
77+
throw FileNotFoundException("Zip file not found: $zipFilePath")
78+
}
79+
80+
try {
81+
ZipFile(zipFilePath.toFile()).use { zipFile ->
82+
zipFile.entries()
83+
.asSequence()
84+
.filterNot { it.isDirectory }
85+
.map { zipEntry ->
86+
val destPath = destDir.resolve(zipEntry.name)
87+
destPath.createParentDirectories()
88+
FileOutputStream(destPath.toFile()).use { targetFile ->
89+
zipFile.getInputStream(zipEntry).copyTo(targetFile)
90+
}
91+
}.toList()
92+
}
93+
} catch (e: Exception) {
94+
throw LspException("Failed to extract zip file: ${e.message}", LspException.ErrorCode.UNZIP_FAILED, cause = e)
95+
}
96+
}

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ManifestFetcher.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts
55

6-
import org.assertj.core.util.VisibleForTesting
6+
import org.jetbrains.annotations.VisibleForTesting
77
import software.aws.toolkits.core.utils.deleteIfExists
88
import software.aws.toolkits.core.utils.error
99
import software.aws.toolkits.core.utils.exists

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/EncoderServer.kt

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,13 @@ import com.nimbusds.jwt.JWTClaimsSet
2121
import com.nimbusds.jwt.SignedJWT
2222
import org.apache.commons.codec.digest.DigestUtils
2323
import software.amazon.awssdk.utils.UserHomeDirectoryUtils
24-
import software.aws.toolkits.core.utils.createParentDirectories
25-
import software.aws.toolkits.core.utils.exists
2624
import software.aws.toolkits.core.utils.getLogger
2725
import software.aws.toolkits.core.utils.info
2826
import software.aws.toolkits.core.utils.tryDirOp
2927
import software.aws.toolkits.core.utils.warn
28+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.extractZipFile
3029
import software.aws.toolkits.jetbrains.services.amazonq.project.manifest.ManifestManager
3130
import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
32-
import java.io.FileOutputStream
3331
import java.io.IOException
3432
import java.nio.file.Files
3533
import java.nio.file.Path
@@ -39,7 +37,6 @@ import java.security.Key
3937
import java.security.SecureRandom
4038
import java.util.Base64
4139
import java.util.concurrent.atomic.AtomicInteger
42-
import java.util.zip.ZipFile
4340
import javax.crypto.spec.SecretKeySpec
4441

4542
class EncoderServer(val project: Project) : Disposable {
@@ -183,7 +180,7 @@ class EncoderServer(val project: Project) : Disposable {
183180
if (serverContent?.url != null) {
184181
if (validateHash(serverContent.hashes?.first(), HttpRequests.request(serverContent.url).readBytes(null))) {
185182
downloadFromRemote(serverContent.url, zipFilePath)
186-
unzipFile(zipFilePath, cachePath)
183+
extractZipFile(zipFilePath, cachePath)
187184
}
188185
}
189186
} catch (e: Exception) {
@@ -231,26 +228,6 @@ class EncoderServer(val project: Project) : Disposable {
231228
Files.setPosixFilePermissions(filePath, permissions)
232229
}
233230

234-
private fun unzipFile(zipFilePath: Path, destDir: Path) {
235-
if (!zipFilePath.exists()) return
236-
try {
237-
val zipFile = ZipFile(zipFilePath.toFile())
238-
zipFile.use { file ->
239-
file.entries().asSequence()
240-
.filterNot { it.isDirectory }
241-
.map { zipEntry ->
242-
val destPath = destDir.resolve(zipEntry.name)
243-
destPath.createParentDirectories()
244-
FileOutputStream(destPath.toFile()).use { targetFile ->
245-
zipFile.getInputStream(zipEntry).copyTo(targetFile)
246-
}
247-
}.toList()
248-
}
249-
} catch (e: Exception) {
250-
logger.warn { "error while unzipping project context artifact: ${e.message}" }
251-
}
252-
}
253-
254231
private fun downloadFromRemote(url: String, path: Path) {
255232
try {
256233
HttpRequests.request(url).saveToFile(path, null)

0 commit comments

Comments
 (0)