Skip to content

Commit 6b2cbdc

Browse files
committed
Seems to work
1 parent 9b9b83b commit 6b2cbdc

File tree

6 files changed

+337
-394
lines changed

6 files changed

+337
-394
lines changed

apps/faf-legacy-deployment/scripts/CoopDeployer.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import org.apache.commons.compress.archivers.zip.Zip64Mode
1+
@file:Suppress("PackageDirectoryMismatch")
2+
3+
package com.faforever.coopdeployer
4+
25
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
36
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
47
import org.eclipse.jgit.api.Git
@@ -18,9 +21,6 @@ import java.security.MessageDigest
1821
import java.sql.Connection
1922
import java.sql.DriverManager
2023
import java.time.Duration
21-
import java.util.zip.CRC32
22-
import java.util.zip.ZipEntry
23-
import java.util.zip.ZipOutputStream
2424
import kotlin.io.path.inputStream
2525

2626
private val log = LoggerFactory.getLogger("CoopDeployer")
@@ -34,7 +34,7 @@ fun Path.setPerm664() {
3434
Files.setPosixFilePermissions(this, perms)
3535
}
3636

37-
data class FeatureModGitRepo(
37+
data class GitRepo(
3838
val workDir: Path,
3939
val repoUrl: String,
4040
val gitRef: String,
@@ -424,7 +424,7 @@ fun main() {
424424

425425
log.info("=== Kotlin Coop Deployer v{} ===", PATCH_VERSION)
426426

427-
val repo = FeatureModGitRepo(
427+
val repo = GitRepo(
428428
workDir = Paths.get(WORKDIR),
429429
repoUrl = REPO_URL,
430430
gitRef = GIT_REF
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
@file:Suppress("PackageDirectoryMismatch")
2+
3+
package com.faforever.coopmapdeployer
4+
5+
import ch.qos.logback.classic.Logger
6+
import ch.qos.logback.classic.Level
7+
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
8+
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
9+
import org.eclipse.jgit.api.Git
10+
import org.slf4j.LoggerFactory
11+
import java.nio.file.Files
12+
import java.nio.file.Path
13+
import java.nio.file.Paths
14+
import java.nio.file.attribute.FileTime
15+
import java.security.MessageDigest
16+
import java.sql.Connection
17+
import java.sql.DriverManager
18+
import kotlin.io.path.copyTo
19+
import kotlin.io.path.createDirectories
20+
import kotlin.io.path.exists
21+
import kotlin.io.path.isDirectory
22+
import kotlin.io.path.isRegularFile
23+
import kotlin.io.path.readBytes
24+
import kotlin.io.path.readText
25+
import kotlin.io.path.walk
26+
27+
private val log = LoggerFactory.getLogger("coop-maps-updater")
28+
29+
30+
private const val FIXED_TIMESTAMP = 1078100502L // 2004-03-01T00:21:42Z
31+
private val FIXED_FILE_TIME = FileTime.fromMillis(FIXED_TIMESTAMP)
32+
33+
data class GitRepo(
34+
val workDir: Path,
35+
val repoUrl: String,
36+
val gitRef: String,
37+
) {
38+
fun checkout(): Path {
39+
if (Files.exists(workDir.resolve(".git"))) {
40+
log.info("Repo exists — fetching and checking out $gitRef...")
41+
Git.open(workDir.toFile()).use { git ->
42+
git.fetch().call()
43+
git.checkout().setName(gitRef).call()
44+
}
45+
} else {
46+
log.info("Cloning repository $repoUrl")
47+
Git.cloneRepository()
48+
.setURI(repoUrl)
49+
.setDirectory(workDir.toFile())
50+
.call()
51+
log.info("Checking out $gitRef")
52+
Git.open(workDir.toFile()).use { git ->
53+
git.checkout().setName(gitRef).call()
54+
}
55+
}
56+
57+
return workDir
58+
}
59+
}
60+
61+
62+
data class CoopMap(
63+
val folderName: String,
64+
val mapId: Int,
65+
val mapType: Int
66+
) {
67+
fun zipName(version: Int) =
68+
"${folderName.lowercase()}.v${version.toString().padStart(4, '0')}.zip"
69+
70+
fun folderName(version: Int) =
71+
"${folderName.lowercase()}.v${version.toString().padStart(4, '0')}"
72+
}
73+
74+
private val coopMaps = listOf(
75+
CoopMap("X1CA_Coop_001", 1, 0),
76+
CoopMap("X1CA_Coop_002", 3, 0),
77+
CoopMap("X1CA_Coop_003", 4, 0),
78+
CoopMap("X1CA_Coop_004", 5, 0),
79+
CoopMap("X1CA_Coop_005", 6, 0),
80+
CoopMap("X1CA_Coop_006", 7, 0),
81+
82+
CoopMap("SCCA_Coop_A01", 8, 1),
83+
CoopMap("SCCA_Coop_A02", 9, 1),
84+
CoopMap("SCCA_Coop_A03", 10, 1),
85+
CoopMap("SCCA_Coop_A04", 11, 1),
86+
CoopMap("SCCA_Coop_A05", 12, 1),
87+
CoopMap("SCCA_Coop_A06", 13, 1),
88+
89+
CoopMap("SCCA_Coop_R01", 20, 2),
90+
CoopMap("SCCA_Coop_R02", 21, 2),
91+
CoopMap("SCCA_Coop_R03", 22, 2),
92+
CoopMap("SCCA_Coop_R04", 23, 2),
93+
CoopMap("SCCA_Coop_R05", 24, 2),
94+
CoopMap("SCCA_Coop_R06", 25, 2),
95+
96+
CoopMap("SCCA_Coop_E01", 14, 3),
97+
CoopMap("SCCA_Coop_E02", 15, 3),
98+
CoopMap("SCCA_Coop_E03", 16, 3),
99+
CoopMap("SCCA_Coop_E04", 17, 3),
100+
CoopMap("SCCA_Coop_E05", 18, 3),
101+
CoopMap("SCCA_Coop_E06", 19, 3),
102+
103+
CoopMap("FAF_Coop_Prothyon_16", 26, 4),
104+
CoopMap("FAF_Coop_Fort_Clarke_Assault", 27, 4),
105+
CoopMap("FAF_Coop_Theta_Civilian_Rescue", 28, 4),
106+
CoopMap("FAF_Coop_Novax_Station_Assault", 31, 4),
107+
CoopMap("FAF_Coop_Operation_Tha_Atha_Aez", 32, 4),
108+
CoopMap("FAF_Coop_Havens_Invasion", 33, 4),
109+
CoopMap("FAF_Coop_Operation_Rescue", 35, 4),
110+
CoopMap("FAF_Coop_Operation_Uhthe_Thuum_QAI", 36, 4),
111+
CoopMap("FAF_Coop_Operation_Yath_Aez", 37, 4),
112+
CoopMap("FAF_Coop_Operation_Ioz_Shavoh_Kael", 38, 4),
113+
CoopMap("FAF_Coop_Operation_Trident", 39, 4),
114+
CoopMap("FAF_Coop_Operation_Blockade", 40, 4),
115+
CoopMap("FAF_Coop_Operation_Golden_Crystals", 41, 4),
116+
CoopMap("FAF_Coop_Operation_Holy_Raid", 42, 4),
117+
CoopMap("FAF_Coop_Operation_Tight_Spot", 45, 4),
118+
CoopMap("FAF_Coop_Operation_Overlord_Surth_Velsok", 47, 4),
119+
CoopMap("FAF_Coop_Operation_Rebels_Rest", 48, 4),
120+
CoopMap("FAF_Coop_Operation_Red_Revenge", 49, 4),
121+
)
122+
123+
data class FafDatabase(
124+
val host: String,
125+
val database: String,
126+
val username: String,
127+
val password: String,
128+
val dryRun: Boolean
129+
) : AutoCloseable {
130+
private val connection: Connection =
131+
DriverManager.getConnection(
132+
"jdbc:mariadb://$host/$database?useSSL=false&serverTimezone=UTC",
133+
username,
134+
password
135+
)
136+
137+
fun getLatestVersion(map: CoopMap): Int {
138+
connection.createStatement().use { st ->
139+
st.executeQuery("SELECT version FROM coop_map WHERE id=${map.mapId}")
140+
.use { rs ->
141+
if (!rs.next()) error("Map ${map.mapId} not found")
142+
return rs.getInt(1)
143+
}
144+
}
145+
}
146+
147+
fun updateDatabase(map: CoopMap, version: Int) {
148+
val sql = """
149+
UPDATE coop_map
150+
SET version=$version,
151+
filename='maps/${map.zipName(version)}'
152+
WHERE id=${map.mapId}
153+
""".trimIndent()
154+
155+
connection.createStatement().use { it.executeUpdate(sql) }
156+
}
157+
158+
override fun close() {
159+
connection.close()
160+
}
161+
}
162+
163+
private fun processCoopMap(
164+
db: FafDatabase,
165+
map: CoopMap,
166+
simulate: Boolean,
167+
gitDir: String,
168+
mapsDir: String
169+
) {
170+
log.info("Processing $map")
171+
172+
val tmp = Files.createTempDirectory("coop-map")
173+
try {
174+
Files.walk(Path.of(gitDir, map.folderName)).forEach {
175+
val target = tmp.resolve(Path.of(gitDir, map.folderName).relativize(it))
176+
if (it.isDirectory()) target.createDirectories()
177+
else it.copyTo(target)
178+
}
179+
180+
val files = tmp.walk().filter { it.isRegularFile() }.toList()
181+
val currentVersion = db.getLatestVersion(map)
182+
183+
val currentZip = Path.of(mapsDir, map.zipName(currentVersion))
184+
val tmpZip = tmp.resolve(map.zipName(currentVersion))
185+
186+
createZip(map, currentVersion, files, tmp, tmpZip)
187+
188+
val changed = currentVersion == 0 ||
189+
!currentZip.exists() ||
190+
md5(currentZip) != md5(tmpZip)
191+
192+
if (!changed) {
193+
log.info("$map unchanged")
194+
return
195+
}
196+
197+
val newVersion = currentVersion + 1
198+
log.info("$map updated → v$newVersion")
199+
200+
if (!simulate) {
201+
val finalZip = Path.of(mapsDir, map.zipName(newVersion))
202+
createZip(map, newVersion, files, tmp, finalZip)
203+
db.updateDatabase(map, newVersion)
204+
}
205+
} finally {
206+
tmp.toFile().deleteRecursively()
207+
}
208+
}
209+
210+
private fun createZip(
211+
map: CoopMap,
212+
version: Int,
213+
files: List<Path>,
214+
base: Path,
215+
out: Path
216+
) {
217+
ZipArchiveOutputStream(out.toFile()).use { zip ->
218+
zip.setMethod(ZipArchiveEntry.DEFLATED)
219+
220+
files.forEach { file ->
221+
val rel = base.relativize(file)
222+
val entryPath = "/${map.folderName(version)}/$rel"
223+
224+
val bytes = file.readText()
225+
.replace(
226+
"/maps/${map.folderName}/",
227+
"/maps/${map.folderName(version)}/"
228+
).toByteArray()
229+
230+
val entry = ZipArchiveEntry(entryPath).apply {
231+
// Ensure deterministic times
232+
setTime(FIXED_FILE_TIME)
233+
setCreationTime(FIXED_FILE_TIME)
234+
setLastModifiedTime(FIXED_FILE_TIME)
235+
setLastAccessTime(FIXED_FILE_TIME)
236+
237+
size = bytes.size.toLong()
238+
}
239+
240+
zip.putArchiveEntry(entry)
241+
zip.write(bytes)
242+
zip.closeArchiveEntry()
243+
}
244+
245+
zip.finish()
246+
}
247+
}
248+
249+
private fun md5(path: Path): String {
250+
val md = MessageDigest.getInstance("MD5")
251+
md.update(path.readBytes())
252+
return md.digest().joinToString("") { "%02x".format(it) }
253+
}
254+
255+
fun main(args: Array<String>) {
256+
val MAP_DIR = System.getenv("MAP_DIR") ?: "/opt/faf/data/faf-coop-maps"
257+
val PATCH_VERSION = System.getenv("PATCH_VERSION") ?: error("PATCH_VERSION required")
258+
val REPO_URL = System.getenv("GIT_REPO_URL") ?: "https://github.com/FAForever/faf-coop-maps"
259+
val GIT_REF = System.getenv("GIT_REF") ?: "v$PATCH_VERSION"
260+
val WORKDIR = System.getenv("GIT_WORKDIR") ?: "/tmp/fa-coop-kt"
261+
val DRYRUN = (System.getenv("DRY_RUN") ?: "false").lowercase() in listOf("1", "true", "yes")
262+
263+
val DB_HOST = System.getenv("DATABASE_HOST") ?: "localhost"
264+
val DB_NAME = System.getenv("DATABASE_NAME") ?: "faf"
265+
val DB_USER = System.getenv("DATABASE_USERNAME") ?: "root"
266+
val DB_PASS = System.getenv("DATABASE_PASSWORD") ?: "banana"
267+
268+
val level = System.getenv("LOG_LEVEL") ?: "INFO"
269+
270+
val root = LoggerFactory
271+
.getLogger(Logger.ROOT_LOGGER_NAME) as Logger
272+
root.level = Level.toLevel(level, Level.INFO)
273+
274+
Files.createDirectories(Paths.get(MAP_DIR))
275+
276+
GitRepo(
277+
workDir = Paths.get(WORKDIR),
278+
repoUrl = REPO_URL,
279+
gitRef = GIT_REF,
280+
).checkout()
281+
282+
FafDatabase(
283+
host = DB_HOST,
284+
database = DB_NAME,
285+
username = DB_USER,
286+
password = DB_PASS,
287+
dryRun = DRYRUN
288+
).use { db ->
289+
coopMaps.forEach {
290+
try {
291+
processCoopMap(db, it, DRYRUN, WORKDIR, MAP_DIR)
292+
} catch (e: Exception) {
293+
log.warn("Failed processing $it", e)
294+
}
295+
}
296+
}
297+
298+
}

apps/faf-legacy-deployment/scripts/build.gradle.kts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,28 @@ dependencies {
1212
implementation("org.eclipse.jgit:org.eclipse.jgit:7.5.0.202512021534-r")
1313
implementation("org.apache.commons:commons-compress:1.28.0")
1414
implementation("org.slf4j:slf4j-api:2.0.13")
15-
runtimeOnly("ch.qos.logback:logback-classic:1.5.23")
16-
}
17-
18-
application {
19-
mainClass.set("CoopDeployerKt") // filename + Kt
15+
implementation("ch.qos.logback:logback-classic:1.5.23")
2016
}
2117

2218
// Use the root level for files
2319
sourceSets {
2420
main {
2521
kotlin.srcDirs(".")
2622
}
23+
}
24+
25+
tasks.register<JavaExec>("deployCoop") {
26+
group = "application"
27+
description = "Deploy coop"
28+
29+
classpath = sourceSets.main.get().runtimeClasspath
30+
mainClass.set("com.faforever.coopdeployer.CoopDeployerKt")
31+
}
32+
33+
tasks.register<JavaExec>("deployCoopMaps") {
34+
group = "application"
35+
description = "Deploy coop maps"
36+
37+
classpath = sourceSets.main.get().runtimeClasspath
38+
mainClass.set("com.faforever.coopmapdeployer.CoopMapDeployerKt")
2739
}

0 commit comments

Comments
 (0)