feat: add Harmony Connector for HarmonyOS 6 USB file transfer
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
// Explicit browser-only UI fixture. Never included in a .curio package.
|
||||
import { createMockBridge } from "./sdk/index.js";
|
||||
const root = 0xffffffff;
|
||||
const device = {
|
||||
id: "fixture",
|
||||
name: "HarmonyOS 6 · 界面测试",
|
||||
manufacturer: "模拟设备",
|
||||
};
|
||||
const storages = [
|
||||
{ id: 1, name: "内部存储", capacity: 256 * 1024 ** 3, free: 128 * 1024 ** 3 },
|
||||
];
|
||||
let connection = null,
|
||||
job = null,
|
||||
next = 30;
|
||||
const entries = [
|
||||
{
|
||||
id: 2,
|
||||
parent: root,
|
||||
storage: 1,
|
||||
name: "Documents",
|
||||
folder: true,
|
||||
size: 0,
|
||||
modified: 0,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
parent: root,
|
||||
storage: 1,
|
||||
name: "DCIM",
|
||||
folder: true,
|
||||
size: 0,
|
||||
modified: 0,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
parent: root,
|
||||
storage: 1,
|
||||
name: "设计稿 🎨 final.zip",
|
||||
folder: false,
|
||||
size: 52_423_111,
|
||||
modified: 1788998400,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
parent: root,
|
||||
storage: 1,
|
||||
name: "<script>alert(1)</script>.txt",
|
||||
folder: false,
|
||||
size: 48,
|
||||
modified: 1788998400,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
parent: 2,
|
||||
storage: 1,
|
||||
name: "旅行计划.pdf",
|
||||
folder: false,
|
||||
size: 1123988,
|
||||
modified: 1788998400,
|
||||
},
|
||||
];
|
||||
window.__TROVE_CONTEXT__ = {
|
||||
curioId: "dev.trove.harmony-connector",
|
||||
version: "0.1.0",
|
||||
instanceId: "ui-fixture",
|
||||
platform: "macos",
|
||||
};
|
||||
window.__TROVE_BRIDGE__ = createMockBridge({
|
||||
"@self.info": () => ({ connection, busy: false }),
|
||||
"@self.devices": () => ({ devices: [device] }),
|
||||
"@self.connect": () => (connection = { device, storages }),
|
||||
"@self.disconnect": () => {
|
||||
connection = null;
|
||||
return { disconnected: true };
|
||||
},
|
||||
"@self.list": ({ parent, offset = 0 }) => {
|
||||
const all = entries.filter((e) => e.parent === parent);
|
||||
return { entries: all.slice(offset, offset + 500), total: all.length };
|
||||
},
|
||||
"@self.mkdir": ({ name, storage, parent }) => {
|
||||
entries.push({
|
||||
id: next++,
|
||||
name,
|
||||
storage,
|
||||
parent,
|
||||
folder: true,
|
||||
size: 0,
|
||||
modified: 0,
|
||||
});
|
||||
return { id: next - 1 };
|
||||
},
|
||||
"@self.transfer": () => {
|
||||
job = { id: 1, state: "running", sent: 0, total: 10 * 1024 ** 2 };
|
||||
return { id: 1 };
|
||||
},
|
||||
"@self.transferStatus": () => {
|
||||
if (job?.state === "running") {
|
||||
job.sent += 524288;
|
||||
if (job.sent >= job.total) {
|
||||
job.state = "completed";
|
||||
job.result = { name: "界面测试文件", bytes: job.total };
|
||||
}
|
||||
}
|
||||
return job;
|
||||
},
|
||||
"@self.cancelTransfer": () => {
|
||||
job.state = "cancelled";
|
||||
job.error = { message: "模拟取消已完成。" };
|
||||
connection = null;
|
||||
return { requested: true };
|
||||
},
|
||||
"host.dialog.openFile": () => ({ path: "/ui-fixture/example.txt" }),
|
||||
"host.dialog.openDirectory": () => ({ path: "/ui-fixture/downloads" }),
|
||||
});
|
||||
const badge = document.createElement("div");
|
||||
badge.textContent = "界面测试 · 模拟设备,不执行真实文件传输";
|
||||
badge.style.cssText =
|
||||
"position:fixed;bottom:3px;left:240px;z-index:10;background:#805800;color:white;padding:4px 8px;border-radius:5px;font-size:10px";
|
||||
document.body.append(badge);
|
||||
await import("./app.js");
|
||||
@@ -0,0 +1,47 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import path from "node:path";
|
||||
import { root, output, run, troveBin } from "./suite.mjs";
|
||||
|
||||
run(process.execPath, ["curios/harmony-connector/build.mjs"]);
|
||||
const directory = path.join(root, "curios/harmony-connector");
|
||||
const manifest = JSON.parse(
|
||||
await readFile(path.join(directory, "curio.json"), "utf8"),
|
||||
);
|
||||
const file = path.join(
|
||||
output,
|
||||
"packages",
|
||||
`${manifest.id}-${manifest.version}-${process.arch}.curio`,
|
||||
);
|
||||
await mkdir(path.dirname(file), { recursive: true });
|
||||
run(troveBin, ["curio", "check", directory, "--json"]);
|
||||
run(troveBin, [
|
||||
"curio",
|
||||
"pack",
|
||||
directory,
|
||||
"--out",
|
||||
file,
|
||||
"--overwrite",
|
||||
"--no-input",
|
||||
"--json",
|
||||
]);
|
||||
const data = await readFile(file);
|
||||
const entries = [
|
||||
{
|
||||
directory: "harmony-connector",
|
||||
manifest,
|
||||
file,
|
||||
artifact: {
|
||||
target: `${process.arch === "arm64" ? "aarch64" : "x86_64"}-apple-darwin`,
|
||||
url: pathToFileURL(file).href,
|
||||
sha256: createHash("sha256").update(data).digest("hex"),
|
||||
size: data.length,
|
||||
},
|
||||
},
|
||||
];
|
||||
await writeFile(
|
||||
path.join(output, "harmony-release.json"),
|
||||
JSON.stringify(entries, null, 2) + "\n",
|
||||
);
|
||||
console.log("Release input: dist/harmony-release.json");
|
||||
@@ -8,6 +8,7 @@ import { appendRelease, artifactPath } from './release-catalog.mjs';
|
||||
const { values } = parseArgs({ options: {
|
||||
'base-url': { type: 'string' },
|
||||
catalog: { type: 'string', default: 'source' },
|
||||
entries: { type: 'string', default: 'dist/suite.json' },
|
||||
} });
|
||||
const base = values['base-url'] ? new URL(values['base-url']) : null;
|
||||
if (base && (base.protocol !== 'https:' || base.username || base.password || base.search || base.hash)) {
|
||||
@@ -31,7 +32,7 @@ for (const spec of specifications) {
|
||||
if (index.id !== spec.id || index.schemaVersion !== 1 || !Array.isArray(index.curios)) throw new Error(`Unexpected catalog identity: ${spec.file}`);
|
||||
catalogs.set(spec.file, index);
|
||||
}
|
||||
const entries = JSON.parse(await readFile(path.join(output, 'suite.json'), 'utf8'));
|
||||
const entries = JSON.parse(await readFile(path.resolve(root, values.entries), 'utf8'));
|
||||
const files = [];
|
||||
// Validate every proposed addition before changing any catalog or copying artifacts.
|
||||
for (const entry of entries) {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { cp, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { createServer } from "node:http";
|
||||
import path from "node:path";
|
||||
import { root, output } from "./suite.mjs";
|
||||
const directory = path.join(output, "harmony-ui-preview");
|
||||
await mkdir(directory, { recursive: true });
|
||||
await cp(path.join(root, "curios/harmony-connector/dist/ui"), directory, {
|
||||
recursive: true,
|
||||
});
|
||||
await cp(
|
||||
path.join(root, "tools/fixtures/harmony-ui.mjs"),
|
||||
path.join(directory, "fixture.mjs"),
|
||||
);
|
||||
const html = (
|
||||
await readFile(path.join(directory, "index.html"), "utf8")
|
||||
).replace('src="./app.js"', 'src="./fixture.mjs"');
|
||||
await writeFile(path.join(directory, "index.html"), html);
|
||||
createServer(async (request, response) => {
|
||||
try {
|
||||
const pathname = new URL(request.url, "http://localhost").pathname;
|
||||
const file = path.resolve(
|
||||
directory,
|
||||
"." + decodeURIComponent(pathname === "/" ? "/index.html" : pathname),
|
||||
);
|
||||
if (!file.startsWith(directory + path.sep)) {
|
||||
response.writeHead(403).end();
|
||||
return;
|
||||
}
|
||||
const data = await readFile(file);
|
||||
response.setHeader(
|
||||
"Content-Type",
|
||||
{
|
||||
".html": "text/html",
|
||||
".css": "text/css",
|
||||
".js": "text/javascript",
|
||||
".mjs": "text/javascript",
|
||||
}[path.extname(file)] || "application/octet-stream",
|
||||
);
|
||||
response.end(data);
|
||||
} catch {
|
||||
response.writeHead(404).end();
|
||||
}
|
||||
}).listen(18770, "127.0.0.1", () =>
|
||||
console.log("UI fixture only: http://127.0.0.1:18770"),
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
import path from "node:path";
|
||||
import { root, run, troveBin } from "./suite.mjs";
|
||||
const plugin = path.join(root, "curios/harmony-connector");
|
||||
run(process.execPath, [path.join(plugin, "build.mjs")]);
|
||||
run(
|
||||
"cargo",
|
||||
[
|
||||
"test",
|
||||
"--locked",
|
||||
"--manifest-path",
|
||||
path.join(plugin, "native/Cargo.toml"),
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
HARMONY_NATIVE_PREFIX: path.join(plugin, "dist/native-deps/install"),
|
||||
MACOSX_DEPLOYMENT_TARGET: "13.0",
|
||||
},
|
||||
},
|
||||
);
|
||||
run(process.execPath, ["--test", path.join(plugin, "ui/format.test.js")]);
|
||||
run(troveBin, [
|
||||
"protocol",
|
||||
"test",
|
||||
"--command",
|
||||
path.join(plugin, "backend/harmony-connector"),
|
||||
]);
|
||||
run(path.join(plugin, "backend/harmony-connector"), ["--diagnose"]);
|
||||
Reference in New Issue
Block a user