Skip to content

Commit 2079fa4

Browse files
committed
fix #3057: re-upload + detail-view now restore broken mirror links
Three intertwined bugs prevented re-uploading the 5 listed maps from fixing their "WILL NOT BE DOWNLOADABLE" state: 1. ProbeAndAssign wrote LinkCount = ok ? 1 : 0 based on the springfiles probe alone, so a local copy on disk could not lift LinkCount above 0 when springfiles 404'd (the actual case for Sever_1.sd7 etc, which springfiles serves only in lowercase). LinkCount now reflects the total mirror count; cache state moves to "cf.Links contains springfilesUrl". 2. UploadResource's .Where(ArchiveName == file.FileName) was case-sensitive, so if unitsync's archive cache reported a different casing than the browser-supplied filename the result was silently dropped and nothing was written. Made case-insensitive. 3. UploadResource copied to res.ResourceInfo.ArchiveName but built the URL from file.FileName, so on case-sensitive FS the URL pointed nowhere. Copy destination now prefers the DB row's canonical FileName so it matches what BuildLinks/File.Exists subsequently probes. Adds ResourceLinkProvider.RefreshLinks(cf): live local check + cached/ deduped springfiles probe, persists if state changed. Called from UploadResource (with LastLinkCheck cleared to force a fresh probe — the user re-uploaded specifically to fix a broken map) and from GetMapDetailData on every detail view (cheap because of the 24 h cache).
1 parent 494e5bd commit 2079fa4

2 files changed

Lines changed: 65 additions & 21 deletions

File tree

Zero-K.info/AppCode/ResourceLinkProvider.cs

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -80,18 +80,41 @@ public static List<string> BuildLinks(ResourceContentFile content)
8080
return links;
8181
}
8282

83+
// Recomputes cf.Links/LinkCount from live local-file check + cached/probed springfiles state.
84+
// Returns true if the entity changed; caller must SaveChanges() in that case.
85+
// Local check is always live (cheap stat); springfiles obeys the 24 h cache and Lazy<bool> dedup.
86+
public static bool RefreshLinks(ResourceContentFile cf)
87+
{
88+
if (cf?.FileName == null || cf.FileName.EndsWith(".sdp") || cf.Resource.MissionID != null) return false;
89+
90+
var prevLinks = cf.Links;
91+
var prevCount = cf.LinkCount;
92+
var prevLastCheck = cf.Resource.LastLinkCheck;
93+
94+
var links = BuildLinks(cf);
95+
cf.Links = string.Join("\n", links);
96+
cf.LinkCount = links.Count;
97+
98+
return cf.Links != prevLinks || cf.LinkCount != prevCount || cf.Resource.LastLinkCheck != prevLastCheck;
99+
}
100+
83101
// 24 h cache + dedup: at most one HEAD per Md5 in flight regardless of caller count.
84-
// LinkCount in DB encodes the cached probe result (1 = springfiles had it, 0 = it didn't).
85-
static bool IsSpringfilesAvailable(ResourceContentFile content, string url)
102+
// Cache state is the presence of springfilesUrl in cf.Links (set by the last probe);
103+
// LinkCount is the total mirror count (local + springfiles), not a springfiles-only flag.
104+
// Updates cf.Resource.LastLinkCheck on the caller's entity after a probe so the caller's
105+
// db context ends up with the same LastLinkCheck the probe wrote in its own context.
106+
static bool IsSpringfilesAvailable(ResourceContentFile content, string springfilesUrl)
86107
{
87108
if (content.Resource.LastLinkCheck.HasValue
88109
&& DateTime.UtcNow.Subtract(content.Resource.LastLinkCheck.Value).TotalHours < SpringfilesRecheckHours)
89-
return content.LinkCount > 0;
110+
return !string.IsNullOrEmpty(content.Links) && content.Links.Split('\n').Contains(springfilesUrl);
90111

91112
var md5 = content.Md5;
92-
return InflightProbes.GetOrAdd(md5, key => new Lazy<bool>(
113+
var result = InflightProbes.GetOrAdd(md5, key => new Lazy<bool>(
93114
() => RunProbe(key),
94115
LazyThreadSafetyMode.ExecutionAndPublication)).Value;
116+
content.Resource.LastLinkCheck = DateTime.UtcNow;
117+
return result;
95118
}
96119

97120
// The cache re-check inside the using block covers the gap between Lazy completion
@@ -107,7 +130,11 @@ static bool RunProbe(string md5)
107130
if (cf == null) return false;
108131
if (cf.Resource.LastLinkCheck.HasValue
109132
&& DateTime.UtcNow.Subtract(cf.Resource.LastLinkCheck.Value).TotalHours < SpringfilesRecheckHours)
110-
return cf.LinkCount > 0;
133+
{
134+
var subfolder = cf.Resource.TypeID == ResourceType.Map ? "maps" : "games";
135+
var springfilesUrl = $"{GlobalConst.SpringfilesBaseUrl}files/{subfolder}/{cf.FileName}";
136+
return !string.IsNullOrEmpty(cf.Links) && cf.Links.Split('\n').Contains(springfilesUrl);
137+
}
111138

112139
var ok = ProbeAndAssign(cf);
113140
db.SaveChanges();
@@ -144,16 +171,16 @@ static bool ProbeAndAssign(ResourceContentFile cf)
144171
var localUrl = $"{GlobalConst.BaseSiteUrl}/content/{subfolder}/{cf.FileName}";
145172
var springfilesUrl = $"{GlobalConst.SpringfilesBaseUrl}files/{subfolder}/{cf.FileName}";
146173

147-
var ok = HeadCheck(springfilesUrl, cf.Length);
174+
var hasSpringfiles = HeadCheck(springfilesUrl, cf.Length);
148175

149176
var links = new List<string>();
150177
if (File.Exists(Global.MapPath($"~/content/{subfolder}/{cf.FileName}"))) links.Add(localUrl);
151-
if (ok) links.Add(springfilesUrl);
178+
if (hasSpringfiles) links.Add(springfilesUrl);
152179

153180
cf.Links = string.Join("\n", links);
154-
cf.LinkCount = ok ? 1 : 0;
181+
cf.LinkCount = links.Count;
155182
cf.Resource.LastLinkCheck = DateTime.UtcNow;
156-
return ok;
183+
return hasSpringfiles;
157184
}
158185

159186
static bool HeadCheck(string url, int length)

Zero-K.info/Controllers/MapsController.cs

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,14 @@ static ZkDataContext FilterMaps(string search,
368368
}
369369

370370
MapDetailData GetMapDetailData(Resource res, ZkDataContext db) {
371+
// opportunistically reconcile mirror state on each view. Local file check is a cheap stat;
372+
// springfiles probe is gated by the 24 h cache + dedup so this only HEADs once a day at most.
373+
// Fixes the case where a file is on disk in content/maps but LinkCount is stale 0 — see #3057.
374+
var anyChanged = false;
375+
foreach (var cf in res.ResourceContentFiles)
376+
if (ResourceLinkProvider.RefreshLinks(cf)) anyChanged = true;
377+
if (anyChanged) db.SaveChanges();
378+
371379
var data = new MapDetailData
372380
{
373381
Resource = res,
@@ -433,33 +441,42 @@ public ActionResult UploadResource(HttpPostedFileBase file, bool specialMap)
433441
try
434442
{
435443
file.SaveAs(tmp);
436-
var results = Global.AutoRegistrator.UnitSyncer.Scan()?.Where(x=>x.ResourceInfo?.ArchiveName == file.FileName)?.ToList();
444+
// case-insensitive match: unitsync's archiveCache may report a different casing
445+
// than the user uploaded; without OrdinalIgnoreCase the filter silently dropped
446+
// the result and nothing got updated — see issue #3057
447+
var results = Global.AutoRegistrator.UnitSyncer.Scan()
448+
?.Where(x => string.Equals(x.ResourceInfo?.ArchiveName, file.FileName, StringComparison.OrdinalIgnoreCase))
449+
?.ToList();
437450
var model = new List<RegistrationResult>();
438451
foreach (var res in results)
439452
{
440453
if (res.Status != UnitSyncer.ResourceFileStatus.RegistrationError)
441454
{
442-
// copy to content subfolder
443455
var subfolder = (res.ResourceInfo is Map) ? "maps" : "games";
444456
var contentFolder = Path.Combine(Server.MapPath("~/content"), subfolder);
445457
if (!Directory.Exists(contentFolder)) Directory.CreateDirectory(contentFolder);
446458

447-
var destFile = Path.Combine(contentFolder, res.ResourceInfo.ArchiveName);
448-
if (!System.IO.File.Exists(destFile)) System.IO.File.Copy(tmp, destFile);
449-
450-
451-
// register as mirror
459+
// refresh mirrors (local + springfiles)
452460
using (var db = new ZkDataContext())
453461
{
454462
var resource = db.Resources.FirstOrDefault(x => x.InternalName == res.ResourceInfo.Name);
455463
// case-insensitive FileName match: DB row may have different casing than the
456464
// freshly-uploaded file (springfiles vs original-upload casing) — see issue #3052
457465
var contentFile = resource?.ResourceContentFiles.FirstOrDefault(
458466
x => string.Equals(x.FileName, file.FileName, StringComparison.OrdinalIgnoreCase));
467+
468+
// copy to content/. Prefer the DB row's canonical FileName so File.Exists
469+
// (case-sensitive on Linux) lines up with what BuildLinks probes — see #3057
470+
var destName = contentFile?.FileName ?? file.FileName;
471+
var destFile = Path.Combine(contentFolder, destName);
472+
if (!System.IO.File.Exists(destFile)) System.IO.File.Copy(tmp, destFile);
473+
459474
if (contentFile != null)
460475
{
461-
contentFile.Links = $"{GlobalConst.BaseSiteUrl}/content/{subfolder}/{file.FileName}";
462-
contentFile.LinkCount = 1;
476+
// force a fresh springfiles probe: the user explicitly re-uploaded to
477+
// fix a broken map, so honor that by bypassing the 24 h cache
478+
contentFile.Resource.LastLinkCheck = null;
479+
ResourceLinkProvider.RefreshLinks(contentFile);
463480
}
464481

465482
// tag as special if required
@@ -470,9 +487,9 @@ public ActionResult UploadResource(HttpPostedFileBase file, bool specialMap)
470487

471488
db.SaveChanges();
472489
}
473-
490+
474491
}
475-
492+
476493
// note this is needed because of some obscure binding issue in asp.net
477494
model.Add(new RegistrationResult()
478495
{
@@ -482,7 +499,7 @@ public ActionResult UploadResource(HttpPostedFileBase file, bool specialMap)
482499
Author = res.ResourceInfo?.Author,
483500
Url = Url.Action("Detail", new {id = new ZkDataContext().Resources.FirstOrDefault(x => x.InternalName == res.ResourceInfo.Name)?.ResourceID})
484501
});
485-
502+
486503
}
487504
return View("UploadResourceResult", model);
488505
}

0 commit comments

Comments
 (0)