com.robotsquid.squidhub 0.5.3 → 0.5.5
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.
- package/CHANGELOG.md +42 -0
- package/Editor/SquidHubBuildScan.cs +165 -0
- package/Editor/SquidHubBuildScan.cs.meta +11 -0
- package/Editor/SquidHubUploadSettings.cs +98 -0
- package/Editor/SquidHubUploadSettings.cs.meta +11 -0
- package/Editor/SquidHubUploader.cs +231 -0
- package/Editor/SquidHubUploader.cs.meta +11 -0
- package/README.md +23 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.5
|
|
4
|
+
|
|
5
|
+
**Upload a build straight from Unity.** `SquidHub → Upload Build…` picks the
|
|
6
|
+
build folder, checks it, and pushes it to SquidHub — no console, no command
|
|
7
|
+
line, no repository checkout.
|
|
8
|
+
|
|
9
|
+
Set it up once in `SquidHub → Upload Settings` with a key from the publisher
|
|
10
|
+
console (your game → **Upload keys**).
|
|
11
|
+
|
|
12
|
+
⚠️ **The key is stored in EditorPrefs, never in your project.** It is per-machine
|
|
13
|
+
and per-project-path, so there is no path by which it reaches your repository —
|
|
14
|
+
a better guarantee than being told not to commit it. A key can do exactly one
|
|
15
|
+
thing: upload a build for the one game it was minted for. It cannot read
|
|
16
|
+
anything, publish to a channel, or touch another game.
|
|
17
|
+
|
|
18
|
+
⚠️ **Uploading is not publishing.** A build arrives registered and not live;
|
|
19
|
+
promoting it to players stays a deliberate act in the console.
|
|
20
|
+
|
|
21
|
+
The build is refused before anything uploads if it is precompressed, or has no
|
|
22
|
+
`index.html` at its top level. `.DS_Store` and friends are dropped rather than
|
|
23
|
+
uploaded — which is also what makes a Unity-pushed build hash identically to the
|
|
24
|
+
same bytes pushed by `squidhub-release` or dragged into the console.
|
|
25
|
+
|
|
26
|
+
## 0.5.4
|
|
27
|
+
|
|
28
|
+
**No functional change from 0.5.3.**
|
|
29
|
+
|
|
30
|
+
Published to supersede an accidental blanket deprecation: `npm deprecate` was
|
|
31
|
+
run without a version specifier, which marks *every* published version rather
|
|
32
|
+
than one, so 0.5.0 through 0.5.3 all carry npm's generic "Package no longer
|
|
33
|
+
supported" text — including the version people should have been installing.
|
|
34
|
+
|
|
35
|
+
Deprecation is per-version and there is no package-level flag, so a new release
|
|
36
|
+
is undeprecated by definition. ⚠️ The older versions keep their misleading
|
|
37
|
+
marking; three of them ought to be deprecated anyway (0.5.0 ships no root
|
|
38
|
+
`.meta` files, 0.5.1 predates the uncompressed-upload model, 0.5.2 rewrites
|
|
39
|
+
compressed asset URLs for a hosting model that no longer exists), just not for
|
|
40
|
+
the reason the text gives.
|
|
41
|
+
|
|
42
|
+
⚠️ **`npm deprecate <package>` with no `@version` applies to everything.** Always
|
|
43
|
+
write `npm deprecate <package>@<version>`.
|
|
44
|
+
|
|
3
45
|
## 0.5.3
|
|
4
46
|
|
|
5
47
|
**Compressed builds are now refused in the Editor**, with a message naming the
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Turning a build folder into the file list the upload API expects.
|
|
2
|
+
//
|
|
3
|
+
// ⚠️ Pure System.* — no UnityEditor, no UnityEngine, no network. That is
|
|
4
|
+
// deliberate: this is the only part of the uploader with real logic, so it
|
|
5
|
+
// stays runnable under plain `dotnet` in tools/test. The same split is what
|
|
6
|
+
// made SquidHubHtml testable.
|
|
7
|
+
using System;
|
|
8
|
+
using System.Collections.Generic;
|
|
9
|
+
using System.IO;
|
|
10
|
+
using System.Linq;
|
|
11
|
+
using System.Security.Cryptography;
|
|
12
|
+
|
|
13
|
+
namespace SquidHub.Editor
|
|
14
|
+
{
|
|
15
|
+
internal sealed class ScannedFile
|
|
16
|
+
{
|
|
17
|
+
/// <summary>Forward-slashed path relative to the build root — the R2 key.</summary>
|
|
18
|
+
public string Path;
|
|
19
|
+
public string Sha256;
|
|
20
|
+
public long Bytes;
|
|
21
|
+
/// <summary>Absolute path, for the upload step.</summary>
|
|
22
|
+
public string FullPath;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
internal sealed class BuildScan
|
|
26
|
+
{
|
|
27
|
+
public readonly List<ScannedFile> Files = new List<ScannedFile>();
|
|
28
|
+
/// <summary>Reasons this build cannot be uploaded. Non-empty means stop.</summary>
|
|
29
|
+
public readonly List<string> Refusals = new List<string>();
|
|
30
|
+
/// <summary>OS metadata dropped. Reported, never silent.</summary>
|
|
31
|
+
public readonly List<string> Skipped = new List<string>();
|
|
32
|
+
public long TotalBytes;
|
|
33
|
+
public bool HasEntryPoint;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
internal static class SquidHubBuildScan
|
|
37
|
+
{
|
|
38
|
+
/// <summary>
|
|
39
|
+
/// ⚠️ Dropped, not refused — and it must match what the CLI and console do.
|
|
40
|
+
/// They hash a directory listing that never contains these, so filtering
|
|
41
|
+
/// here is what makes all three agree on a buildHash.
|
|
42
|
+
/// </summary>
|
|
43
|
+
private static bool IsJunk(string relativePath)
|
|
44
|
+
{
|
|
45
|
+
var name = relativePath.Split('/').Last();
|
|
46
|
+
return name == ".DS_Store" || name == "Thumbs.db" || relativePath.StartsWith("__MACOSX/");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// <summary>
|
|
50
|
+
/// ⚠️ Precompressed builds cannot be served — Cloudflare either strips
|
|
51
|
+
/// Content-Encoding or compresses the body a second time. The server
|
|
52
|
+
/// refuses these too and is the authority; refusing here means finding
|
|
53
|
+
/// out before uploading a couple of hundred megabytes.
|
|
54
|
+
/// </summary>
|
|
55
|
+
private static string RefusalFor(string relativePath)
|
|
56
|
+
{
|
|
57
|
+
foreach (var pair in new[] { (".br", "Brotli"), (".gz", "gzip"), (".zz", "deflate") })
|
|
58
|
+
{
|
|
59
|
+
if (!relativePath.EndsWith(pair.Item1, StringComparison.OrdinalIgnoreCase)) continue;
|
|
60
|
+
return relativePath + " is " + pair.Item2 + "-compressed. SquidHub serves build " +
|
|
61
|
+
"assets uncompressed and Cloudflare compresses them at the edge. Set Player " +
|
|
62
|
+
"Settings > Publishing Settings > Compression Format to Disabled and rebuild.";
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public static BuildScan Scan(string root)
|
|
68
|
+
{
|
|
69
|
+
var scan = new BuildScan();
|
|
70
|
+
var full = System.IO.Path.GetFullPath(root);
|
|
71
|
+
|
|
72
|
+
foreach (var file in Directory.GetFiles(full, "*", SearchOption.AllDirectories).OrderBy(p => p))
|
|
73
|
+
{
|
|
74
|
+
// ⚠️ Backslashes normalised. A path hashed as `Build\x.wasm` on
|
|
75
|
+
// Windows and `Build/x.wasm` elsewhere is the same bytes producing
|
|
76
|
+
// two different buildHashes.
|
|
77
|
+
var relative = file.Substring(full.Length).TrimStart('/', '\\').Replace('\\', '/');
|
|
78
|
+
|
|
79
|
+
if (IsJunk(relative))
|
|
80
|
+
{
|
|
81
|
+
scan.Skipped.Add(relative);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
var refusal = RefusalFor(relative);
|
|
86
|
+
if (refusal != null)
|
|
87
|
+
{
|
|
88
|
+
scan.Refusals.Add(refusal);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
var bytes = File.ReadAllBytes(file);
|
|
93
|
+
scan.Files.Add(new ScannedFile
|
|
94
|
+
{
|
|
95
|
+
Path = relative,
|
|
96
|
+
Sha256 = ToHex(SHA256.Create().ComputeHash(bytes)),
|
|
97
|
+
Bytes = bytes.LongLength,
|
|
98
|
+
FullPath = file,
|
|
99
|
+
});
|
|
100
|
+
scan.TotalBytes += bytes.LongLength;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ⚠️ Checked before anything uploads. The portal frames
|
|
104
|
+
// /v/<hash>/index.html, so a build without one registers perfectly
|
|
105
|
+
// and then fails to launch for every player.
|
|
106
|
+
scan.HasEntryPoint = scan.Files.Any(f => f.Path == "index.html");
|
|
107
|
+
if (!scan.HasEntryPoint)
|
|
108
|
+
{
|
|
109
|
+
scan.Refusals.Add(
|
|
110
|
+
"No index.html at the top level of " + root + ". The game cannot launch. " +
|
|
111
|
+
"Point this at the folder Unity built into, not at its parent.");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return scan;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private static string ToHex(byte[] bytes)
|
|
118
|
+
{
|
|
119
|
+
var chars = new char[bytes.Length * 2];
|
|
120
|
+
for (var i = 0; i < bytes.Length; i++)
|
|
121
|
+
{
|
|
122
|
+
chars[i * 2] = "0123456789abcdef"[bytes[i] >> 4];
|
|
123
|
+
chars[i * 2 + 1] = "0123456789abcdef"[bytes[i] & 0xF];
|
|
124
|
+
}
|
|
125
|
+
return new string(chars);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// <summary>
|
|
129
|
+
/// The `files` array for the start request.
|
|
130
|
+
///
|
|
131
|
+
/// ⚠️ Hand-built rather than via a JSON library, for the same reason the
|
|
132
|
+
/// generated wrapper writes its own: a Unity project already carries one
|
|
133
|
+
/// and shipping a second is a second parser to maintain.
|
|
134
|
+
/// </summary>
|
|
135
|
+
public static string FilesJson(IEnumerable<ScannedFile> files)
|
|
136
|
+
{
|
|
137
|
+
var parts = files.Select(f =>
|
|
138
|
+
"{\"path\":" + JsonString(f.Path) +
|
|
139
|
+
",\"sha256\":\"" + f.Sha256 + "\"" +
|
|
140
|
+
",\"bytes\":" + f.Bytes + "}");
|
|
141
|
+
return "[" + string.Join(",", parts) + "]";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
internal static string JsonString(string value)
|
|
145
|
+
{
|
|
146
|
+
var sb = new System.Text.StringBuilder("\"");
|
|
147
|
+
foreach (var c in value)
|
|
148
|
+
{
|
|
149
|
+
switch (c)
|
|
150
|
+
{
|
|
151
|
+
case '"': sb.Append("\\\""); break;
|
|
152
|
+
case '\\': sb.Append("\\\\"); break;
|
|
153
|
+
case '\n': sb.Append("\\n"); break;
|
|
154
|
+
case '\r': sb.Append("\\r"); break;
|
|
155
|
+
case '\t': sb.Append("\\t"); break;
|
|
156
|
+
default:
|
|
157
|
+
if (c < ' ') sb.Append("\\u").Append(((int)c).ToString("x4"));
|
|
158
|
+
else sb.Append(c);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return sb.Append('"').ToString();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Where the upload key lives, and where it must not.
|
|
2
|
+
//
|
|
3
|
+
// ⚠️ EditorPrefs, NEVER a file in the project. A key written under Assets/ ends
|
|
4
|
+
// up in the publisher's git repository, and from there in anyone's clone. This
|
|
5
|
+
// is per-machine and per-user by construction, so there is no path by which
|
|
6
|
+
// committing it is possible — which is a better guarantee than telling people
|
|
7
|
+
// not to.
|
|
8
|
+
using UnityEditor;
|
|
9
|
+
using UnityEngine;
|
|
10
|
+
|
|
11
|
+
namespace SquidHub.Editor
|
|
12
|
+
{
|
|
13
|
+
internal static class SquidHubUploadSettings
|
|
14
|
+
{
|
|
15
|
+
// ⚠️ Keyed per project path, so two games open on one machine cannot
|
|
16
|
+
// upload to each other by inheriting a key.
|
|
17
|
+
private static string Scope => "SquidHub." + Application.dataPath.GetHashCode() + ".";
|
|
18
|
+
|
|
19
|
+
private const string DefaultEndpoint = "https://publish.robotsquid.dev";
|
|
20
|
+
|
|
21
|
+
public static string Key
|
|
22
|
+
{
|
|
23
|
+
get => EditorPrefs.GetString(Scope + "uploadKey", "");
|
|
24
|
+
set => EditorPrefs.SetString(Scope + "uploadKey", value.Trim());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public static string Endpoint
|
|
28
|
+
{
|
|
29
|
+
get => EditorPrefs.GetString(Scope + "endpoint", DefaultEndpoint).TrimEnd('/');
|
|
30
|
+
set => EditorPrefs.SetString(Scope + "endpoint", value.Trim().TrimEnd('/'));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public static string BuildFolder
|
|
34
|
+
{
|
|
35
|
+
get => EditorPrefs.GetString(Scope + "buildFolder", "");
|
|
36
|
+
set => EditorPrefs.SetString(Scope + "buildFolder", value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public static bool HasKey => Key.StartsWith("shu_");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
internal sealed class SquidHubUploadSettingsWindow : EditorWindow
|
|
43
|
+
{
|
|
44
|
+
private string _key = "";
|
|
45
|
+
private string _endpoint = "";
|
|
46
|
+
|
|
47
|
+
[MenuItem("SquidHub/Upload Settings", priority = 10)]
|
|
48
|
+
private static void Open()
|
|
49
|
+
{
|
|
50
|
+
var window = GetWindow<SquidHubUploadSettingsWindow>(true, "SquidHub Upload", true);
|
|
51
|
+
window._key = SquidHubUploadSettings.Key;
|
|
52
|
+
window._endpoint = SquidHubUploadSettings.Endpoint;
|
|
53
|
+
window.minSize = new Vector2(460, 210);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private void OnGUI()
|
|
57
|
+
{
|
|
58
|
+
EditorGUILayout.HelpBox(
|
|
59
|
+
"Create a key in the publisher console under your game → Upload keys. " +
|
|
60
|
+
"It can only upload builds for that one game.\n\n" +
|
|
61
|
+
"The key is stored in EditorPrefs on this machine, never in your project, " +
|
|
62
|
+
"so it cannot reach your repository.",
|
|
63
|
+
MessageType.Info);
|
|
64
|
+
|
|
65
|
+
// ⚠️ A password field. Not because it is secret from the person
|
|
66
|
+
// typing it, but because it is pasted during screen shares and
|
|
67
|
+
// recorded tutorials more often than anyone expects.
|
|
68
|
+
_key = EditorGUILayout.PasswordField("Upload key", _key);
|
|
69
|
+
_endpoint = EditorGUILayout.TextField("Console URL", _endpoint);
|
|
70
|
+
|
|
71
|
+
EditorGUILayout.Space();
|
|
72
|
+
using (new EditorGUILayout.HorizontalScope())
|
|
73
|
+
{
|
|
74
|
+
if (GUILayout.Button("Save"))
|
|
75
|
+
{
|
|
76
|
+
SquidHubUploadSettings.Key = _key;
|
|
77
|
+
SquidHubUploadSettings.Endpoint = string.IsNullOrWhiteSpace(_endpoint)
|
|
78
|
+
? "https://publish.robotsquid.dev"
|
|
79
|
+
: _endpoint;
|
|
80
|
+
Close();
|
|
81
|
+
}
|
|
82
|
+
if (GUILayout.Button("Clear key"))
|
|
83
|
+
{
|
|
84
|
+
SquidHubUploadSettings.Key = "";
|
|
85
|
+
_key = "";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!string.IsNullOrEmpty(_key) && !_key.StartsWith("shu_"))
|
|
90
|
+
{
|
|
91
|
+
EditorGUILayout.HelpBox(
|
|
92
|
+
"That does not look like an upload key — they begin with shu_. " +
|
|
93
|
+
"A console session cookie will not work here.",
|
|
94
|
+
MessageType.Warning);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// SquidHub → Upload Build. The whole publish, from the Editor.
|
|
2
|
+
//
|
|
3
|
+
// ⚠️ Runs the same job API the console does: start, PUT each file, complete.
|
|
4
|
+
// Only the authentication differs — a per-game upload key instead of a session
|
|
5
|
+
// cookie — so a build pushed from here is validated by exactly the same server
|
|
6
|
+
// code as one dragged into the console.
|
|
7
|
+
//
|
|
8
|
+
// ⚠️ Thin ON PURPOSE. Everything with real logic lives in SquidHubBuildScan,
|
|
9
|
+
// which has no UnityEditor dependency and is unit-tested under plain dotnet.
|
|
10
|
+
// This file is plumbing, and plumbing is what cannot be tested here.
|
|
11
|
+
using System;
|
|
12
|
+
using System.Collections;
|
|
13
|
+
using System.IO;
|
|
14
|
+
using System.Text;
|
|
15
|
+
using System.Text.RegularExpressions;
|
|
16
|
+
using UnityEditor;
|
|
17
|
+
using UnityEngine;
|
|
18
|
+
using UnityEngine.Networking;
|
|
19
|
+
|
|
20
|
+
namespace SquidHub.Editor
|
|
21
|
+
{
|
|
22
|
+
internal static class SquidHubUploader
|
|
23
|
+
{
|
|
24
|
+
private static IEnumerator _routine;
|
|
25
|
+
|
|
26
|
+
[MenuItem("SquidHub/Upload Build…", priority = 11)]
|
|
27
|
+
private static void UploadBuild()
|
|
28
|
+
{
|
|
29
|
+
if (!SquidHubUploadSettings.HasKey)
|
|
30
|
+
{
|
|
31
|
+
EditorUtility.DisplayDialog("SquidHub",
|
|
32
|
+
"No upload key set. Create one in the publisher console under your game → " +
|
|
33
|
+
"Upload keys, then paste it into SquidHub → Upload Settings.", "OK");
|
|
34
|
+
SquidHubUploadSettingsWindow.GetWindow<SquidHubUploadSettingsWindow>();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
var start = SquidHubUploadSettings.BuildFolder;
|
|
39
|
+
var folder = EditorUtility.OpenFolderPanel(
|
|
40
|
+
"Select the WebGL build folder (containing index.html)",
|
|
41
|
+
string.IsNullOrEmpty(start) ? "" : start, "");
|
|
42
|
+
if (string.IsNullOrEmpty(folder)) return;
|
|
43
|
+
SquidHubUploadSettings.BuildFolder = folder;
|
|
44
|
+
|
|
45
|
+
BuildScan scan;
|
|
46
|
+
try
|
|
47
|
+
{
|
|
48
|
+
scan = SquidHubBuildScan.Scan(folder);
|
|
49
|
+
}
|
|
50
|
+
catch (Exception e)
|
|
51
|
+
{
|
|
52
|
+
EditorUtility.DisplayDialog("SquidHub", "Could not read that folder:\n\n" + e.Message, "OK");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ⚠️ Refusals stop the upload before a byte moves. Every one of them
|
|
57
|
+
// is something the server would reject anyway; finding out here
|
|
58
|
+
// saves the publisher the round trip and names the fix.
|
|
59
|
+
if (scan.Refusals.Count > 0)
|
|
60
|
+
{
|
|
61
|
+
EditorUtility.DisplayDialog("SquidHub — build not uploadable",
|
|
62
|
+
string.Join("\n\n", scan.Refusals), "OK");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!EditorUtility.DisplayDialog("SquidHub",
|
|
67
|
+
$"Upload {scan.Files.Count} files ({scan.TotalBytes / 1024f / 1024f:F1} MB)?" +
|
|
68
|
+
(scan.Skipped.Count > 0
|
|
69
|
+
? $"\n\n{scan.Skipped.Count} OS metadata file(s) will be skipped."
|
|
70
|
+
: ""),
|
|
71
|
+
"Upload", "Cancel"))
|
|
72
|
+
{
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
Run(Upload(scan));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ⚠️ Driven by EditorApplication.update rather than blocking. Sleeping
|
|
80
|
+
// the main thread would stop UnityWebRequest progressing at all, and the
|
|
81
|
+
// Editor would appear to hang for the length of the upload.
|
|
82
|
+
private static void Run(IEnumerator routine)
|
|
83
|
+
{
|
|
84
|
+
_routine = routine;
|
|
85
|
+
EditorApplication.update -= Pump;
|
|
86
|
+
EditorApplication.update += Pump;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private static void Pump()
|
|
90
|
+
{
|
|
91
|
+
try
|
|
92
|
+
{
|
|
93
|
+
if (_routine != null && _routine.MoveNext()) return;
|
|
94
|
+
}
|
|
95
|
+
catch (Exception e)
|
|
96
|
+
{
|
|
97
|
+
EditorUtility.ClearProgressBar();
|
|
98
|
+
Debug.LogError("[SquidHub] upload failed: " + e);
|
|
99
|
+
}
|
|
100
|
+
_routine = null;
|
|
101
|
+
EditorApplication.update -= Pump;
|
|
102
|
+
EditorUtility.ClearProgressBar();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private static IEnumerator Upload(BuildScan scan)
|
|
106
|
+
{
|
|
107
|
+
var endpoint = SquidHubUploadSettings.Endpoint;
|
|
108
|
+
var key = SquidHubUploadSettings.Key;
|
|
109
|
+
|
|
110
|
+
EditorUtility.DisplayProgressBar("SquidHub", "Registering build…", 0f);
|
|
111
|
+
|
|
112
|
+
var startBody = "{\"intent\":\"start\",\"files\":" + SquidHubBuildScan.FilesJson(scan.Files) + "}";
|
|
113
|
+
string startResponse = null;
|
|
114
|
+
yield return Post(endpoint + "/api/ingest", key, startBody, r => startResponse = r);
|
|
115
|
+
|
|
116
|
+
if (startResponse == null) yield break;
|
|
117
|
+
|
|
118
|
+
if (Field(startResponse, "alreadyPublished") == "true")
|
|
119
|
+
{
|
|
120
|
+
EditorUtility.ClearProgressBar();
|
|
121
|
+
EditorUtility.DisplayDialog("SquidHub",
|
|
122
|
+
"These exact bytes are already published as " + Field(startResponse, "buildHash") +
|
|
123
|
+
". Nothing to upload.", "OK");
|
|
124
|
+
yield break;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
var jobId = Field(startResponse, "jobId");
|
|
128
|
+
if (string.IsNullOrEmpty(jobId))
|
|
129
|
+
{
|
|
130
|
+
Fail("The server did not start a build job", startResponse);
|
|
131
|
+
yield break;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (var i = 0; i < scan.Files.Count; i++)
|
|
135
|
+
{
|
|
136
|
+
var file = scan.Files[i];
|
|
137
|
+
EditorUtility.DisplayProgressBar("SquidHub",
|
|
138
|
+
$"Uploading {i + 1}/{scan.Files.Count} — {file.Path}",
|
|
139
|
+
(float)i / scan.Files.Count);
|
|
140
|
+
|
|
141
|
+
var request = new UnityWebRequest(endpoint + "/api/ingest/upload", "PUT");
|
|
142
|
+
request.uploadHandler = new UploadHandlerRaw(File.ReadAllBytes(file.FullPath));
|
|
143
|
+
request.downloadHandler = new DownloadHandlerBuffer();
|
|
144
|
+
request.SetRequestHeader("authorization", "Bearer " + key);
|
|
145
|
+
request.SetRequestHeader("x-squidhub-job", jobId);
|
|
146
|
+
request.SetRequestHeader("x-squidhub-path", file.Path);
|
|
147
|
+
request.SetRequestHeader("x-squidhub-sha256", file.Sha256);
|
|
148
|
+
|
|
149
|
+
var op = request.SendWebRequest();
|
|
150
|
+
while (!op.isDone) yield return null;
|
|
151
|
+
|
|
152
|
+
if (request.result != UnityWebRequest.Result.Success)
|
|
153
|
+
{
|
|
154
|
+
Fail("Upload failed on " + file.Path, request.downloadHandler.text);
|
|
155
|
+
yield break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
EditorUtility.DisplayProgressBar("SquidHub", "Verifying and registering…", 0.99f);
|
|
160
|
+
|
|
161
|
+
string doneResponse = null;
|
|
162
|
+
yield return Post(endpoint + "/api/ingest",
|
|
163
|
+
key,
|
|
164
|
+
"{\"intent\":\"complete\",\"jobId\":" + SquidHubBuildScan.JsonString(jobId) + "}",
|
|
165
|
+
r => doneResponse = r);
|
|
166
|
+
|
|
167
|
+
EditorUtility.ClearProgressBar();
|
|
168
|
+
if (doneResponse == null) yield break;
|
|
169
|
+
|
|
170
|
+
if (Field(doneResponse, "ok") != "true")
|
|
171
|
+
{
|
|
172
|
+
Fail("The server refused the build", doneResponse);
|
|
173
|
+
yield break;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ⚠️ Registered, NOT live. Promoting a build to players is a separate,
|
|
177
|
+
// deliberate act in the console — saying "uploaded" without saying
|
|
178
|
+
// that invites someone to assume their players already have it.
|
|
179
|
+
EditorUtility.DisplayDialog("SquidHub",
|
|
180
|
+
"Registered " + Field(doneResponse, "buildHash") + ".\n\n" +
|
|
181
|
+
"It is not live yet — promote it in the publisher console when you are ready.",
|
|
182
|
+
"OK");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private static IEnumerator Post(string url, string key, string body, Action<string> onOk)
|
|
186
|
+
{
|
|
187
|
+
var request = new UnityWebRequest(url, "POST");
|
|
188
|
+
request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(body));
|
|
189
|
+
request.downloadHandler = new DownloadHandlerBuffer();
|
|
190
|
+
request.SetRequestHeader("authorization", "Bearer " + key);
|
|
191
|
+
request.SetRequestHeader("content-type", "application/json");
|
|
192
|
+
|
|
193
|
+
var op = request.SendWebRequest();
|
|
194
|
+
while (!op.isDone) yield return null;
|
|
195
|
+
|
|
196
|
+
if (request.responseCode == 401)
|
|
197
|
+
{
|
|
198
|
+
Fail("The upload key was refused",
|
|
199
|
+
"Check it in SquidHub → Upload Settings, and that it has not been revoked.");
|
|
200
|
+
yield break;
|
|
201
|
+
}
|
|
202
|
+
if (request.result != UnityWebRequest.Result.Success)
|
|
203
|
+
{
|
|
204
|
+
Fail("Request failed", request.error + "\n" + request.downloadHandler.text);
|
|
205
|
+
yield break;
|
|
206
|
+
}
|
|
207
|
+
onOk(request.downloadHandler.text);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private static void Fail(string title, string detail)
|
|
211
|
+
{
|
|
212
|
+
EditorUtility.ClearProgressBar();
|
|
213
|
+
Debug.LogError("[SquidHub] " + title + ": " + detail);
|
|
214
|
+
EditorUtility.DisplayDialog("SquidHub — " + title, detail, "OK");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/// <summary>
|
|
218
|
+
/// ⚠️ A regex, not a JSON parser, and only for the four flat fields this
|
|
219
|
+
/// flow reads. Shipping a parser in a package that already asks the
|
|
220
|
+
/// project to bring its own would be a second, worse one to maintain.
|
|
221
|
+
/// </summary>
|
|
222
|
+
private static string Field(string json, string name)
|
|
223
|
+
{
|
|
224
|
+
var match = Regex.Match(json, "\"" + Regex.Escape(name) + "\"\\s*:\\s*(\"([^\"]*)\"|true|false|[0-9]+)");
|
|
225
|
+
if (!match.Success) return null;
|
|
226
|
+
return match.Groups[2].Success && match.Value.Contains("\"" + match.Groups[2].Value + "\"")
|
|
227
|
+
? match.Groups[2].Value
|
|
228
|
+
: match.Groups[1].Value;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
package/README.md
CHANGED
|
@@ -82,6 +82,29 @@ Then push the folder:
|
|
|
82
82
|
node src/cli.mjs push --game <slug> --dir <BuildFolder> --env=dev --verify
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
## Uploading from Unity
|
|
86
|
+
|
|
87
|
+
`SquidHub → Upload Build…` does the whole publish — no console, no command line,
|
|
88
|
+
no repository checkout.
|
|
89
|
+
|
|
90
|
+
**Once:** in the publisher console open your game → **Upload keys** → create one,
|
|
91
|
+
then paste it into `SquidHub → Upload Settings`.
|
|
92
|
+
|
|
93
|
+
⚠️ **The key never enters your project.** It lives in EditorPrefs, per machine
|
|
94
|
+
and per project path, so it cannot reach your repository. It can do exactly one
|
|
95
|
+
thing: upload a build for the one game it was minted for — it cannot read
|
|
96
|
+
anything, publish to a channel, or touch another game. If it leaks, revoke it in
|
|
97
|
+
the console; the worst it could have done is leave an unwanted build somewhere
|
|
98
|
+
no player can see.
|
|
99
|
+
|
|
100
|
+
⚠️ **Uploading is not publishing.** The build arrives registered and not live.
|
|
101
|
+
Promoting it to players stays a deliberate act in the console.
|
|
102
|
+
|
|
103
|
+
The upload is refused before a byte moves if the build is precompressed or has
|
|
104
|
+
no `index.html` at its top level, and OS metadata is dropped rather than sent —
|
|
105
|
+
which is also what makes a build pushed from Unity hash identically to the same
|
|
106
|
+
bytes dragged into the console.
|
|
107
|
+
|
|
85
108
|
## In the Editor
|
|
86
109
|
|
|
87
110
|
`Sdk` compiles on every platform and reports `Status() == "failed"` anywhere
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "com.robotsquid.squidhub",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"displayName": "SquidHub",
|
|
5
5
|
"description": "Cloud saves, analytics and the coin economy for Unity WebGL games published on SquidHub. Installs the C# wrapper, the .jslib half and the page bridge, and rewrites the exported index.html so it survives the platform's CSP.",
|
|
6
6
|
"unity": "2021.3",
|