craft-native 0.0.197 → 0.0.199

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -917,7 +917,7 @@ internal object CraftNativeMedia {
917
917
  }
918
918
 
919
919
  fun local(context: Context, address: String): File? =
920
- address.takeIf { it.isNotBlank() }?.let { file(context, it) }?.takeIf(File::exists)
920
+ address.takeIf { it.isNotBlank() }?.let { file(context, it) }?.takeIf { it.isFile && it.length() > 0 }
921
921
 
922
922
  private fun describe(address: String, file: File) = JSONObject().put("ok", true).put("url", address).put("path", file.absolutePath).put("size", file.length())
923
923
 
@@ -951,15 +951,24 @@ internal object CraftNativeMedia {
951
951
  headers.keys().forEach { name -> connection.setRequestProperty(name, headers.optString(name)) }
952
952
  try {
953
953
  val status = connection.responseCode
954
- if (status !in 200..299) return@runCatching CraftNativeCapabilityResult(data = JSONObject().put("ok", false).put("url", address).put("status", status))
954
+ if (status != 200) return@runCatching CraftNativeCapabilityResult(data = JSONObject().put("ok", false).put("url", address).put("status", status))
955
955
  val target = file(context, address)
956
956
  target.parentFile?.mkdirs()
957
957
  // Written aside and moved into place, so a half-written file is never played.
958
- val staging = File(target.parentFile, "${java.util.UUID.randomUUID()}.part")
959
- connection.inputStream.use { input -> staging.outputStream().use { input.copyTo(it) } }
960
- if (!staging.renameTo(target)) {
958
+ val staging = File(target.parentFile, "${target.name}.part")
959
+ try {
960
+ val size = connection.inputStream.use { input -> staging.outputStream().use { input.copyTo(it) } }
961
+ val expected = connection.contentLengthLong
962
+ if (size <= 0 || (expected >= 0 && size != expected)) {
963
+ return@runCatching CraftNativeCapabilityResult(code = "NETWORK_ERROR", message = "media.keep received an incomplete file")
964
+ }
965
+ synchronized(inFlight) {
966
+ if (!staging.renameTo(target)) {
967
+ return@runCatching CraftNativeCapabilityResult(code = "IO_ERROR", message = "media.keep could not store the file")
968
+ }
969
+ }
970
+ } finally {
961
971
  staging.delete()
962
- return@runCatching CraftNativeCapabilityResult(code = "IO_ERROR", message = "media.keep could not store the file")
963
972
  }
964
973
  CraftNativeCapabilityResult(data = describe(address, target))
965
974
  } finally {
@@ -983,7 +992,14 @@ internal object CraftNativeMedia {
983
992
  fun prune(context: Context, keep: List<String>): Int {
984
993
  val wanted = keep.map { file(context, it).name }.toSet()
985
994
  var removed = 0
986
- root(context).listFiles()?.forEach { file -> if (file.name !in wanted && file.delete()) removed++ }
995
+ synchronized(inFlight) {
996
+ // A concurrent keep owns its target and staging file until it finishes.
997
+ val active = inFlight.keys.map { file(context, it).name }.toSet()
998
+ root(context).listFiles()?.forEach { file ->
999
+ val target = file.name.removeSuffix(".part")
1000
+ if (target !in active && file.name !in wanted && file.delete()) removed++
1001
+ }
1002
+ }
987
1003
  return removed
988
1004
  }
989
1005
 
@@ -1640,7 +1640,7 @@ export interface LiveActivityState {
1640
1640
  timerStartedAt?: number | null;
1641
1641
  timerEndsAt?: number | null;
1642
1642
  paused?: boolean;
1643
- remainingSeconds?: number;
1643
+ remainingSeconds?: number | null;
1644
1644
  heartRate?: number | null;
1645
1645
  showsDistance?: boolean;
1646
1646
  staleAt?: number | null;
package/dist/cli.js CHANGED
@@ -3424,7 +3424,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
3424
3424
  `);
3425
3425
  }
3426
3426
  // package.json
3427
- var version = "0.0.197";
3427
+ var version = "0.0.199";
3428
3428
 
3429
3429
  // bin/cli.ts
3430
3430
  var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
package/dist/index.cjs CHANGED
@@ -309,7 +309,7 @@ function craftBinaryNotFoundMessage(triedPath) {
309
309
  `);
310
310
  }
311
311
  // package.json
312
- var version = "0.0.197";
312
+ var version = "0.0.199";
313
313
 
314
314
  // src/sdk-version.ts
315
315
  var SDK_VERSION = version;
package/dist/index.js CHANGED
@@ -57,7 +57,7 @@ function craftBinaryNotFoundMessage(triedPath) {
57
57
  `);
58
58
  }
59
59
  // package.json
60
- var version = "0.0.197";
60
+ var version = "0.0.199";
61
61
 
62
62
  // src/sdk-version.ts
63
63
  var SDK_VERSION = version;
@@ -25,6 +25,7 @@ struct CraftLiveActivityWidget: Widget {
25
25
  metrics(context).font(.caption.monospacedDigit())
26
26
  }
27
27
  }
28
+ .foregroundStyle(.white)
28
29
  .padding()
29
30
  .activityBackgroundTint(Color(red: 0.07, green: 0.10, blue: 0.16))
30
31
  .activitySystemActionForegroundColor(.white)
@@ -515,7 +515,7 @@ enum CraftNativeMedia {
515
515
  static func local(_ address: String) -> URL? {
516
516
  guard !address.isEmpty else { return nil }
517
517
  let url = file(for: address)
518
- return FileManager.default.fileExists(atPath: url.path) ? url : nil
518
+ return ((try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? 0) > 0 ? url : nil
519
519
  }
520
520
 
521
521
  static func describe(_ address: String, _ url: URL) -> [String: Any] {
@@ -545,11 +545,23 @@ enum CraftNativeMedia {
545
545
  /// A finished download moved into place (atomically, so a half-written
546
546
  /// file is never played), or a refusal with the server's status.
547
547
  static func store(_ address: String, at location: URL, response: HTTPURLResponse) -> Result<Any, CraftNativeActionError> {
548
- guard (200..<300).contains(response.statusCode) else {
548
+ guard response.statusCode == 200 else {
549
549
  return .success(["ok": false, "url": address, "status": response.statusCode])
550
550
  }
551
551
  let fm = FileManager.default
552
+ // Commit and prune share a lock; neither can remove a file being committed.
553
+ lock.lock()
554
+ defer { lock.unlock() }
555
+ let staging = root.appendingPathComponent(UUID().uuidString + ".part")
556
+ defer { try? fm.removeItem(at: staging) }
552
557
  do {
558
+ let size = (try location.resourceValues(forKeys: [.fileSizeKey])).fileSize ?? 0
559
+ // URLSession normally rejects a truncated transfer; also guard this boundary.
560
+ let expected = response.expectedContentLength
561
+ let encoded = response.value(forHTTPHeaderField: "Content-Encoding")
562
+ if size <= 0 || (encoded == nil && expected >= 0 && Int64(size) != expected) {
563
+ return .failure(CraftNativeActionError(code: "NETWORK_ERROR", message: "media.keep received an incomplete file"))
564
+ }
553
565
  var folder = root
554
566
  if !fm.fileExists(atPath: folder.path) {
555
567
  try fm.createDirectory(at: folder, withIntermediateDirectories: true)
@@ -558,7 +570,6 @@ enum CraftNativeMedia {
558
570
  try? folder.setResourceValues(values)
559
571
  }
560
572
  let target = file(for: address)
561
- let staging = root.appendingPathComponent(UUID().uuidString + ".part")
562
573
  try fm.moveItem(at: location, to: staging)
563
574
  _ = try? fm.removeItem(at: target)
564
575
  try fm.moveItem(at: staging, to: target)
@@ -571,10 +582,13 @@ enum CraftNativeMedia {
571
582
 
572
583
  /// Removes every copy whose address is not in `keep`; answers how many.
573
584
  static func prune(keep: [String]) -> Int {
585
+ lock.lock()
586
+ defer { lock.unlock() }
574
587
  let wanted = Set(keep.map { file(for: $0).lastPathComponent })
575
588
  let files = (try? FileManager.default.contentsOfDirectory(at: root, includingPropertiesForKeys: nil)) ?? []
576
589
  var removed = 0
577
- for url in files where !wanted.contains(url.lastPathComponent) {
590
+ let active = Set(inFlight.keys.map { file(for: $0).lastPathComponent })
591
+ for url in files where !wanted.contains(url.lastPathComponent) && !active.contains(url.lastPathComponent) {
578
592
  if (try? FileManager.default.removeItem(at: url)) != nil { removed += 1 }
579
593
  }
580
594
  return removed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craft-native",
3
- "version": "0.0.197",
3
+ "version": "0.0.199",
4
4
  "type": "module",
5
5
  "description": "Build desktop apps with web languages - TypeScript SDK for Craft",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",