tina4ruby 3.13.74 → 3.13.75

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ddc9319e2cc05c631d4f456805dccecaf55f2cee47906a06624ff93c7f778c97
4
- data.tar.gz: 871df06b691800d7eed0e9693b5b2ebecc131d0dec5c00084aa603142f0fb35d
3
+ metadata.gz: f7b6cfa07830b59619c790f1724802af5989b3013f7408aa47b35a8ff0dd6ca1
4
+ data.tar.gz: 6b107ed9e12d657b5b3ef8e8183a2cb05c1b7c2e3befcc35cd977b9ddabb63ec
5
5
  SHA512:
6
- metadata.gz: 132581be6195fb4a7d9a90ac086903c1c4466776075ee0cdcef7256647ba9e147e3d14c7a198fbba1ef3594f2c9ec06497ce560ea3540763f0033526930157d6
7
- data.tar.gz: da580bca2f664d573b0dc628fd2b90f7885bcf3cb58b72a49c74e2f1a240e343122de9e75f4e351fcccd5635d8cc2ecba725115d98c205f13816298680b98c91
6
+ metadata.gz: 966951da4e142e1083cb21b9123ae44f0f3c1e5750aef15f5f1d27c91fb1c2119e3212d6a26f560705a20ee341286b6b828d0d36dd0ddcbaaf0dc51264f9faa6
7
+ data.tar.gz: 831737c53898556428116fdf4992c50f1d658acf21f86209aebf00e09b691162b915504992a2f6a396ee0ff13f46720bc9d4896ef841f373a717e437f860a9d7
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require "json"
3
3
  require "securerandom"
4
+ require "time"
4
5
  require "uri"
5
6
 
6
7
  module Tina4
@@ -128,7 +129,7 @@ module Tina4
128
129
  end
129
130
 
130
131
  # Static files (only for non-API paths)
131
- static_response = try_static(path)
132
+ static_response = try_static(path, env)
132
133
  return static_response if static_response
133
134
  end
134
135
 
@@ -386,30 +387,94 @@ module Tina4
386
387
  final_response.to_rack
387
388
  end
388
389
 
389
- def try_static(path)
390
+ def try_static(path, env = nil)
390
391
  return nil if path.include?("..")
391
392
 
392
393
  @static_roots.each do |root|
393
394
  full_path = File.join(root, path)
394
395
  if File.file?(full_path)
395
- return serve_static_file(full_path)
396
+ return serve_static_file(full_path, env)
396
397
  end
397
398
 
398
399
  # Only try index.html for directory-like paths
399
400
  if path.end_with?("/") || !path.include?(".")
400
401
  index_path = File.join(full_path, "index.html")
401
402
  if File.file?(index_path)
402
- return serve_static_file(index_path)
403
+ return serve_static_file(index_path, env)
403
404
  end
404
405
  end
405
406
  end
406
407
  nil
407
408
  end
408
409
 
409
- def serve_static_file(full_path)
410
+ # Serve a static asset with cache-revalidation semantics (parity with the
411
+ # Python master). A static file may be cached by the browser but MUST be
412
+ # revalidated before use, so a redeployed asset reaches the browser on the
413
+ # next load without a manual hard refresh — and an unchanged asset costs a
414
+ # cheap 304 round-trip, not a re-download.
415
+ #
416
+ # Validators are derived from the file's mtime + size (a weak ETag) plus a
417
+ # Last-Modified date, so we never have to read/hash the bytes to build them.
418
+ # When the request already carries a matching validator (If-None-Match, or
419
+ # If-Modified-Since not older than the file) we answer a bare 304 with no
420
+ # body. Stdlib only — no new dependency.
421
+ def serve_static_file(full_path, env = nil)
410
422
  ext = File.extname(full_path).downcase
411
423
  content_type = Tina4::Response::MIME_TYPES[ext] || "application/octet-stream"
412
- [200, { "content-type" => content_type }, [File.binread(full_path)]]
424
+
425
+ stat = File.stat(full_path)
426
+ etag = %(W/"#{stat.mtime.to_i.to_s(16)}-#{stat.size.to_s(16)}")
427
+ last_modified = stat.mtime.httpdate
428
+
429
+ headers = {
430
+ "content-type" => content_type,
431
+ "cache-control" => "no-cache, must-revalidate",
432
+ "etag" => etag,
433
+ "last-modified" => last_modified
434
+ }
435
+
436
+ if env && static_not_modified?(env, etag, stat.mtime)
437
+ # 304 carries the validators (RFC 9110 §15.4.5) but NO body.
438
+ return [304, {
439
+ "cache-control" => "no-cache, must-revalidate",
440
+ "etag" => etag,
441
+ "last-modified" => last_modified
442
+ }, []]
443
+ end
444
+
445
+ [200, headers, [File.binread(full_path)]]
446
+ end
447
+
448
+ # Decide whether a conditional GET for a static asset can be answered 304.
449
+ # If-None-Match takes precedence over If-Modified-Since (RFC 9110 §13.1.3);
450
+ # ETag comparison is weak (a leading `W/` is ignored on either side).
451
+ def static_not_modified?(env, etag, mtime)
452
+ if_none_match = env["HTTP_IF_NONE_MATCH"]
453
+ if if_none_match && !if_none_match.empty?
454
+ return true if if_none_match.strip == "*"
455
+
456
+ want = weak_etag_value(etag)
457
+ return if_none_match.split(",").any? { |candidate| weak_etag_value(candidate) == want }
458
+ end
459
+
460
+ if_modified_since = env["HTTP_IF_MODIFIED_SINCE"]
461
+ if if_modified_since && !if_modified_since.empty?
462
+ begin
463
+ # HTTP dates have 1-second resolution — compare at whole seconds so a
464
+ # sub-second mtime never spuriously looks "newer" than the client copy.
465
+ return mtime.to_i <= Time.httpdate(if_modified_since).to_i
466
+ rescue ArgumentError
467
+ return false # Unparseable date — treat as no validator, serve the body.
468
+ end
469
+ end
470
+
471
+ false
472
+ end
473
+
474
+ # Normalise an ETag for weak comparison: drop a leading `W/` and surrounding
475
+ # whitespace so `W/"abc"` and `"abc"` compare equal.
476
+ def weak_etag_value(tag)
477
+ tag.to_s.strip.sub(/\AW\//, "")
413
478
  end
414
479
 
415
480
  def serve_swagger_ui
data/lib/tina4/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tina4
4
- VERSION = "3.13.74"
4
+ VERSION = "3.13.75"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tina4ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.13.74
4
+ version: 3.13.75
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tina4 Team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-07-13 00:00:00.000000000 Z
11
+ date: 2026-07-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack