@@ -3,22 +3,29 @@ package com.coder.gateway.sdk
3
3
import com.coder.gateway.views.steps.CoderWorkspacesStepView
4
4
import com.intellij.openapi.diagnostic.Logger
5
5
import org.zeroturnaround.exec.ProcessExecutor
6
- import java.io.InputStream
6
+ import java.io.BufferedInputStream
7
+ import java.io.FileInputStream
8
+ import java.io.FileNotFoundException
9
+ import java.net.HttpURLConnection
7
10
import java.net.URL
8
11
import java.nio.file.Files
9
12
import java.nio.file.Path
10
13
import java.nio.file.Paths
14
+ import java.nio.file.StandardCopyOption
11
15
import java.nio.file.attribute.PosixFilePermissions
16
+ import java.security.DigestInputStream
17
+ import java.security.MessageDigest
18
+ import javax.xml.bind.annotation.adapters.HexBinaryAdapter
19
+
12
20
13
21
/* *
14
22
* Manage the CLI for a single deployment.
15
23
*/
16
- class CoderCLIManager (deployment : URL , buildVersion : String ) {
24
+ class CoderCLIManager (deployment : URL ) {
17
25
private var remoteBinaryUrl: URL
18
26
var localBinaryPath: Path
19
27
private var binaryNamePrefix: String
20
28
private var destinationDir: Path
21
- private var localBinaryName: String
22
29
23
30
init {
24
31
// TODO: Should use a persistent path to avoid needing to download on
@@ -28,23 +35,21 @@ class CoderCLIManager(deployment: URL, buildVersion: String) {
28
35
val os = getOS()
29
36
binaryNamePrefix = getCoderCLIForOS(os, getArch())
30
37
val binaryName = if (os == OS .WINDOWS ) " $binaryNamePrefix .exe" else binaryNamePrefix
31
- localBinaryName =
32
- if (os == OS .WINDOWS ) " ${binaryNamePrefix} -${buildVersion} .exe" else " ${binaryNamePrefix} -${buildVersion} "
33
38
remoteBinaryUrl = URL (
34
39
deployment.protocol,
35
40
deployment.host,
36
41
deployment.port,
37
42
" /bin/$binaryName "
38
43
)
39
- localBinaryPath = destinationDir.resolve(localBinaryName )
44
+ localBinaryPath = destinationDir.resolve(binaryName )
40
45
}
41
46
42
47
/* *
43
48
* Return the name of the binary (sans extension) for the provided OS and
44
49
* architecture.
45
50
*/
46
51
private fun getCoderCLIForOS (os : OS ? , arch : Arch ? ): String {
47
- logger.info(" Resolving coder cli for $os $arch " )
52
+ logger.info(" Resolving binary for $os $arch " )
48
53
if (os == null ) {
49
54
logger.error(" Could not resolve client OS and architecture, defaulting to WINDOWS AMD64" )
50
55
return " coder-windows-amd64"
@@ -75,42 +80,59 @@ class CoderCLIManager(deployment: URL, buildVersion: String) {
75
80
* Download the CLI from the deployment if necessary.
76
81
*/
77
82
fun downloadCLI (): Boolean {
78
- Files .createDirectories(destinationDir)
79
- try {
80
- logger.info(" Downloading Coder CLI to ${localBinaryPath.toAbsolutePath()} " )
81
- remoteBinaryUrl.openStream().use {
82
- Files .copy(it as InputStream , localBinaryPath)
83
- }
84
- } catch (e: java.nio.file.FileAlreadyExistsException ) {
85
- // This relies on the provided build version being the latest. It
86
- // must be freshly fetched immediately before downloading.
87
- // TODO: Use etags instead?
88
- logger.info(" ${localBinaryPath.toAbsolutePath()} already exists, skipping download" )
89
- return false
83
+ val etag = getBinaryETag()
84
+ val conn = remoteBinaryUrl.openConnection() as HttpURLConnection
85
+ if (etag != null ) {
86
+ logger.info(" Found existing binary at ${localBinaryPath.toAbsolutePath()} ; calculated hash as $etag " )
87
+ conn.setRequestProperty(" If-None-Match" , " \" $etag \" " )
90
88
}
91
- if (getOS() != OS .WINDOWS ) {
92
- Files .setPosixFilePermissions(
93
- localBinaryPath,
94
- PosixFilePermissions .fromString(" rwxr-x---" )
95
- )
89
+ conn.connect()
90
+ logger.info(" GET ${conn.responseCode} $remoteBinaryUrl " )
91
+ when (conn.responseCode) {
92
+ 200 -> {
93
+ logger.info(" Downloading binary to ${localBinaryPath.toAbsolutePath()} " )
94
+ Files .createDirectories(destinationDir)
95
+ conn.inputStream.use {
96
+ Files .copy(it, localBinaryPath, StandardCopyOption .REPLACE_EXISTING )
97
+ }
98
+ if (getOS() != OS .WINDOWS ) {
99
+ Files .setPosixFilePermissions(
100
+ localBinaryPath,
101
+ PosixFilePermissions .fromString(" rwxr-x---" )
102
+ )
103
+ }
104
+ conn.disconnect()
105
+ return true
106
+ }
107
+
108
+ 304 -> {
109
+ logger.info(" Using cached binary at ${localBinaryPath.toAbsolutePath()} " )
110
+ conn.disconnect()
111
+ return false
112
+ }
96
113
}
97
- return true
114
+ conn.disconnect()
115
+ throw Exception (" Unable to download $remoteBinaryUrl (got response code `${conn.responseCode} `)" )
98
116
}
99
117
100
118
/* *
101
- * Remove all versions of the CLI for this deployment that do not match the
102
- * current build version.
119
+ * Return the entity tag for the binary on disk, if any.
103
120
*/
104
- fun removeOldCli () {
105
- if (Files .isReadable(destinationDir)) {
106
- Files .walk(destinationDir, 1 ).use {
107
- it.sorted().map { pt -> pt.toFile() }
108
- .filter { fl -> fl.name.contains(binaryNamePrefix) && fl.name != localBinaryName }
109
- .forEach { fl ->
110
- logger.info(" Removing $fl because it is an old version" )
111
- fl.delete()
112
- }
121
+ private fun getBinaryETag (): String? {
122
+ try {
123
+ val md = MessageDigest .getInstance(" SHA-1" )
124
+ val fis = FileInputStream (localBinaryPath.toFile())
125
+ val dis = DigestInputStream (BufferedInputStream (fis), md)
126
+ fis.use {
127
+ while (dis.read() != - 1 ) {
128
+ }
113
129
}
130
+ return HexBinaryAdapter ().marshal(md.digest()).lowercase()
131
+ } catch (e: FileNotFoundException ) {
132
+ return null
133
+ } catch (e: Exception ) {
134
+ logger.warn(" Unable to calculate hash for ${localBinaryPath.toAbsolutePath()} " , e)
135
+ return null
114
136
}
115
137
}
116
138
0 commit comments