git » sdk » commit bb5b41e

Prepare a file attachment, including HTTP upload

author Stephen Paul Weber
2023-11-29 16:42:20 UTC
committer Stephen Paul Weber
2023-12-11 15:59:51 UTC
parent 049e71c5e90731750bb054d17c264b7cfa22dbc8

Prepare a file attachment, including HTTP upload

Taking in js.html.File right now, later this should be an abstract with
very similiar properties.

Using tink_http and tink_io in order to get streaming send, we don't
want to load the whole attachment into RAM.

Using sha package for hashes also to get streaming.

Send hash along with HTTP upload slot request, no one uses this but they
could and we have it anyway.

xmpp/Client.hx +42 -0
xmpp/queries/HttpUploadSlot.hx +61 -0

diff --git a/xmpp/Client.hx b/xmpp/Client.hx
index f9a659e..8ff6cfb 100644
--- a/xmpp/Client.hx
+++ b/xmpp/Client.hx
@@ -1,5 +1,7 @@
 package xmpp;
 
+import sha.SHA256;
+
 import haxe.crypto.Base64;
 import haxe.io.Bytes;
 import haxe.io.BytesData;
@@ -16,6 +18,7 @@ import xmpp.queries.DiscoInfoGet;
 import xmpp.queries.DiscoItemsGet;
 import xmpp.queries.ExtDiscoGet;
 import xmpp.queries.GenericQuery;
+import xmpp.queries.HttpUploadSlot;
 import xmpp.queries.JabberIqGatewayGet;
 import xmpp.queries.PubsubGet;
 import xmpp.queries.Push2Enable;
@@ -473,6 +476,45 @@ class Client extends xmpp.EventEmitter {
 		this.stream.trigger("auth/password", { password: password, requestToken: fastMechanism });
 	}
 
+	public function prepareAttachment(source: js.html.File, callback: (Null<ChatAttachment>)->Void) { // TODO: abstract with filename, mime, and ability to convert to tink.io.Source
+		persistence.findServicesWithFeature(accountId(), "urn:xmpp:http:upload:0", (services) -> {
+			final sha256 = new sha.SHA256();
+			tink.io.Source.ofJsFile(source.name, source).chunked().forEach((chunk) -> {
+				sha256.update(chunk);
+				return tink.streams.Stream.Handled.Resume;
+			}).handle((o) -> switch o {
+				case Depleted:
+					prepareAttachmentFor(source, services, [{ algo: "sha-256", hash: sha256.digest().getData() }], callback);
+				default:
+					trace("Error computing attachment hash", o);
+					callback(null);
+			});
+		});
+	}
+
+	private function prepareAttachmentFor(source: js.html.File, services: Array<{ serviceId: String }>, hashes: Array<{algo: String, hash: BytesData}>, callback: (Null<ChatAttachment>)->Void) {
+		if (services.length < 1) {
+			callback(null);
+			return;
+		}
+		final httpUploadSlot = new HttpUploadSlot(services[0].serviceId, source.name, source.size, source.type, hashes);
+		httpUploadSlot.onFinished(() -> {
+			final slot = httpUploadSlot.getResult();
+			if (slot == null) {
+				prepareAttachmentFor(source, services.slice(1), hashes, callback);
+			} else {
+				tink.http.Client.fetch(slot.put, { method: PUT, headers: slot.putHeaders, body: tink.io.Source.RealSourceTools.idealize(tink.io.Source.ofJsFile(source.name, source), (e) -> throw e) }).all()
+					.handle((o) -> switch o {
+						case Success(res) if (res.header.statusCode == 201):
+							callback(new ChatAttachment(source.name, source.type, source.size, [slot.get], hashes));
+						default:
+							prepareAttachmentFor(source, services.slice(1), hashes, callback);
+					});
+			}
+		});
+		sendQuery(httpUploadSlot);
+	}
+
 	/* Return array of chats, sorted by last activity */
 	public function getChats():Array<Chat> {
 		return chats.filter((chat) -> chat.uiState != Closed);
diff --git a/xmpp/queries/HttpUploadSlot.hx b/xmpp/queries/HttpUploadSlot.hx
new file mode 100644
index 0000000..881a346
--- /dev/null
+++ b/xmpp/queries/HttpUploadSlot.hx
@@ -0,0 +1,61 @@
+package xmpp.queries;
+
+import haxe.DynamicAccess;
+import haxe.Exception;
+import haxe.crypto.Base64;
+import haxe.io.Bytes;
+import haxe.io.BytesData;
+
+import xmpp.ID;
+import xmpp.JID;
+import xmpp.ResultSet;
+import xmpp.Stanza;
+import xmpp.Stream;
+import xmpp.queries.GenericQuery;
+
+class HttpUploadSlot extends GenericQuery {
+	public var xmlns(default, null) = "urn:xmpp:http:upload:0";
+	public var queryId:String = null;
+	public var responseStanza(default, null):Stanza;
+	private var result: { put: String, putHeaders: Array<tink.http.Header.HeaderField>, get: String };
+
+	public function new(to: String, filename: String, size: Int, mime: String, hashes: Array<{algo:String,hash:BytesData}>) {
+		/* Build basic query */
+		queryId = ID.short();
+		queryStanza = new Stanza(
+			"iq",
+			{ to: to, type: "get", id: queryId }
+		).tag("request", { xmlns: xmlns, filename: filename, size: Std.string(size), "content-type": mime });
+		for (hash in hashes) {
+			queryStanza.textTag("hash", Base64.encode(Bytes.ofData(hash.hash)), { xmlns: "urn:xmpp:hashes:2", algo: hash.algo });
+		}
+		queryStanza.up();
+	}
+
+	public function handleResponse(stanza:Stanza) {
+		responseStanza = stanza;
+		finish();
+	}
+
+	public function getResult() {
+		if (responseStanza == null) {
+			return null;
+		}
+		if(result == null) {
+			final q = responseStanza.getChild("slot", xmlns);
+			if(q == null) {
+				return null;
+			}
+			final get = q.findText("get@url");
+			if (get == null) return null;
+			final put = q.findText("put@url");
+			if (put == null) return null;
+			final headers = [];
+			for (header in q.getChild("put").allTags("header")) {
+				headers.push(new tink.http.Header.HeaderField(header.attr.get("name"), header.getText()));
+			}
+			result = { get: get, put: put, putHeaders: headers };
+		}
+		return result;
+	}
+}