Comment publier des images via API ?

J’ai travaillé sur un plugin pour Obsidian et j’ai eu beaucoup de mal à faire fonctionner le téléchargement d’images. Voici ce que j’ai jusqu’à présent :

	async uploadImages(imageReferences: string[]): Promise<string[]> {
		const imageUrls = [];
		for (const ref of imageReferences) {
			const filePath = this.app.metadataCache.getFirstLinkpathDest(ref, this.activeFile.name)?.path;
			if (filePath) {
				const file = this.app.vault.getAbstractFileByPath(filePath) as TFile;
				if (file) {
					try {
						const arrayBuffer = await this.app.vault.readBinary(file);
						const blob = new Blob([arrayBuffer]);
						const boundary = '----WebKitFormBoundary7MA4YWxkTrZu0gW';
						let body = '';

						body += `--${boundary}\r\n`;
						body += `Content-Disposition: form-data; name=\"type\"\r\n\r\n`;
						body += "composer\r\n";
						body += `--${boundary}\r\n`;
						body += `Content-Disposition: form-data; name=\"synchronous\"\r\n\r\n`;
						body += "true\r\n";

						body += `--${boundary}\r\n`;
						body += `Content-Disposition: form-data; name=\"files[]\"; filename=\"${file.name}\"\r\n`;
						body += `Content-Type: image/jpg\r\n\r\n`
						body += blob + '\r\n';
						body += `--${boundary}--\r\n`;
						console.log(body)
						const formData = new TextEncoder().encode(body)

						const url = `${this.settings.baseUrl}/uploads.json`;
						const headers = {
							"Api-Key": this.settings.apiKey,
							"Api-Username": this.settings.disUser,
							"Content-Type": `multipart/form-data; boundary=${boundary}`
						};

						const response = await requestUrl({
							url: url,
							method: "POST",
							body: formData,
							throw: false,
							headers: headers,
						});

						//const response = await fetch(url, {
						//	method: "POST",
						//	body: formData,
						//	headers: new Headers(headers),
						//});

						console.log(`Upload Image response: ${response.status}`);
						//if (response.ok) {
						if (response.status == 200) {
							const jsonResponse = response.json();
							console.log(`Upload Image jsonResponse: ${JSON.stringify(jsonResponse)}`);
							imageUrls.push(jsonResponse.url);
						} else {
							new NotifyUser(this.app, `Error uploading image: ${response.status}`).open();
							console.error(`Error uploading image: ${JSON.stringify(response.json)}`);
							//console.error("Error uploading image:", response.status, await response.text());
						}
					} catch (error) {
						new NotifyUser(this.app, `Exception while uploading image: ${error}`).open();
						console.error("Exception while uploading image:", error);
					}
				} else {
					new NotifyUser(this.app, `File not found in vault: ${ref}`).open();
					console.error(`File not found in vault: ${ref}`);
				}
			} else {
				new NotifyUser(this.app, `Unable to resolve file path for: ${ref}`).open();
				console.error(`Unable to resolve file path for: ${ref}`);
			}
		}
		return imageUrls;
	}

Je construis un multipart/form-data car requestURL() ne peut pas accepter un formData() comme paramètre. Seuls string ou arrayBuffer sont acceptés. Je ne peux pas utiliser fetch() car j’obtiens une erreur CORS. Avec ce code (et de nombreux ajustements mineurs au body), j’obtiens l’erreur suivante :

Error uploading image: {“errors”:[“You supplied invalid parameters to the request: Discourse::InvalidParameters”],“error_type”:“invalid_parameters”}

1 « J'aime »

Je pense que je vais procéder à la mise à jour car je reçois maintenant un message d’erreur différent :

	async uploadImages(imageReferences: string[]): Promise<string[]> {
		const imageUrls = [];
		for (const ref of imageReferences) {
			const filePath = this.app.metadataCache.getFirstLinkpathDest(ref, this.activeFile.name)?.path;
			if (filePath) {
				const file = this.app.vault.getAbstractFileByPath(filePath) as TFile;
				if (file) {
					try {
						const imgfile = await this.app.vault.readBinary(file);
						const boundary = genBoundary();
						const sBoundary = '--' + boundary + '\r\n';
						let body = '';
						body += `${sBoundary}Content-Disposition: form-data; name=\"type\"\r\n\r\ncomposer\r\n`;
						body += `${sBoundary}Content-Disposition: form-data; name=\"synchronous\"\r\n\r\ntrue\r\n`;
						body += `${sBoundary}Content-Disposition: form-data; name=\"files[]\"; filename=\"${file.name}\"\r\nContent-Type: image/jpg`;
						console.log(body);

						const eBoundary = '\r\n--' + boundary + '--\\r\\n';
						const bodyArray = new TextEncoder().encode(body);
						const endBoundaryArray = new TextEncoder().encode(eBoundary);

						const formDataArray = new Uint8Array(bodyArray.length + imgfile.byteLength + endBoundaryArray.length);
						formDataArray.set(bodyArray, 0);
						formDataArray.set(new Uint8Array(imgfile), bodyArray.length);
						formDataArray.set(endBoundaryArray, bodyArray.length + imgfile.byteLength);

						const url = `${this.settings.baseUrl}/uploads.json`;
						const headers = {
							"Api-Key": this.settings.apiKey,
							"Api-Username": this.settings.disUser,
							"Content-Type": `multipart/form-data; boundary=${boundary}`
						};

						const response = await requestUrl({
							url: url,
							method: "POST",
							body: formDataArray.buffer,
							throw: false,
							headers: headers,
						});

						console.log(`Upload Image response: ${response.status}`);
						if (response.status == 200) {
							const jsonResponse = response.json();
							console.log(`Upload Image jsonResponse: ${JSON.stringify(jsonResponse)}`);
							imageUrls.push(jsonResponse.url);
						} else {
							new NotifyUser(this.app, `Error uploading image: ${response.status}`).open();
							console.error(`Error uploading image: ${JSON.stringify(response.json)}`);
						}
					} catch (error) {
						new NotifyUser(this.app, `Exception while uploading image: ${error}`).open();
						console.error("Exception while uploading image:", error);
					}
				} else {
					new NotifyUser(this.app, `File not found in vault: ${ref}`).open();
					console.error(`File not found in vault: ${ref}`);
				}
			} else {
				new NotifyUser(this.app, `Unable to resolve file path for: ${ref}`).open();
				console.error(`Unable to resolve file path for: ${ref}`);
			}
		}
		return imageUrls;
	}

Le message d’erreur que je reçois maintenant est :

Exception while uploading image: SyntaxError: Unexpected token ‘I’, "Invalid request" is not valid JSON

C’est déroutant pour moi car l’API indique que nous devons envoyer un multipart/form-data, mais elle indique un JSON invalide ? C’est peut-être lié à requestAPI().

J’ai donc pensé essayer une approche différente en utilisant le stockage S3. J’ai suivi les instructions ici pour configurer un bucket AWS S3. Voici mes paramètres :

Et voici mon code :

	async uploadExternalImage(imageReferences: string[]): Promise<string[]> {
		const imageUrls: string[] = [];
		for (const ref of imageReferences) {
			const filePath = this.app.metadataCache.getFirstLinkpathDest(ref, this.activeFile.name)?.path;
			if (filePath) {
				const file = this.app.vault.getAbstractFileByPath(filePath) as TFile;
				if (file) {
					try {
						const url = `${this.settings.baseUrl}/uploads/generate-presigned-put.json`;
						//const imgfile = await this.app.vault.readBinary(file);
						const img = {
							type: "composer",
							file_name: file.name,
							file_size: file.stat.size,
						}
						console.log(JSON.stringify(img));
						const headers = {
							"Content-Type": "application/json",
							"Api-Key": this.settings.apiKey,
							"Api-Username": this.settings.disUser,
						};
						const response = await requestUrl({
							url: url,
							method: "POST",
							body: JSON.stringify(img),
							throw: false,
							headers: headers
						})
						console.log(response.json)
					} catch (error) {
						console.error(`Error uploading: ${error}`);
						//console.log(response.json)
					}
				} else {
					console.error('error')
				}
			} else {
				console.error('error')
			}
		}
		return imageUrls;
	}

Maintenant, je me rends compte que cela ne fonctionnera pas actuellement car je n’envoie pas réellement le fichier. D’après ce que j’ai lu dans la documentation, j’enverrais un objet json contenant le type, le nom du fichier et la taille du fichier. L’API devrait répondre avec une clé et une URL que j’utiliserais pour le transfert de fichier réel. Mais à ce stade, je reçois l’erreur suivante :

{
    "errors": [
        "The requested URL or resource could not be found."
    ],
    "error_type": "not_found"
}
[
    "The requested URL or resource could not be found."
]

J’ai vérifié ma clé API pour m’assurer qu’elle avait les permissions, c’est le cas. Mais j’en ai créé une nouvelle de toute façon. Et une globale. Aucune ne fonctionne. Même code d’erreur. Qu’est-ce que je fais de mal ?

Edit, voici l’objet img :

{"type":"composer","file_name":"face2.jpg","file_size":17342}

Je ne suis pas très familier avec cet écosystème, mais peut-être que cela peut aider ?

Erreur CORS…

Avez-vous suivi

?

1 « J'aime »

Je fais une supposition, mais je ne pense pas que la configuration de CORS fonctionnera dans ce cas. L’origine (du moins depuis l’application Obsidian Desktop) est 'app://obsidian.md'. Je pense que CORS ne peut être configuré que sur Discourse pour traiter les requêtes HTTP.

@maxtim, avez-vous besoin que cela fonctionne depuis le mobile, ou serait-il suffisant de pouvoir poster sur Discourse depuis l’application de bureau ? Je suppose encore un peu, mais… si je comprends bien, l’application de bureau est une application Electron. Elle fonctionne sur une combinaison de Chromium et Node.js. Vous pourriez être en mesure d’utiliser node-fetch pour effectuer des requêtes côté serveur vers Discourse. Si cela fonctionne, cela résoudrait le problème CORS et vous permettrait d’utiliser FormData dans les requêtes.

J’ai avancé et j’ai essayé (proverbialement jeter des nouilles mouillées pour voir ce qui colle). Mais j’étais déjà appréhensif et @simon a raison.

Idéalement, le plugin serait également disponible sur mobile. Mais pour l’instant, si le bureau uniquement est ce que nous obtenons, c’est ce que nous obtenons.

Bien sûr, une autre solution pourrait être : Obsidian Vault sur mobile → synchronisation vers le bureau → cli pour télécharger sur Discourse. Mais cela semble un peu compliqué.

Fondamentalement, la situation idéale est que le forum Discourse remplace l’Obsidian Vault. De cette façon, les utilisateurs qui préfèrent le forum peuvent utiliser le forum. Les utilisateurs qui préfèrent (ou ont en fait besoin d’une solution hors ligne) peuvent utiliser Obsidian. J’ai déjà quelques idées sur la façon dont une synchronisation bidirectionnelle pourrait fonctionner. Mais je pense que les images/fichiers doivent d’abord être gérés d’une manière ou d’une autre.

Modifier :

Je suis assez convaincu que cela fonctionnera, mais je n’arrive pas à obtenir les bons paramètres :

					try {
						const imgfile = await this.app.vault.readBinary(file);
						const boundary = genBoundary();
						const sBoundary = '--' + boundary + '\r\n';
						let body = '';
						body += `${sBoundary}Content-Disposition: form-data; name=\"type\"\r\n\r\ncomposer\r\n`;
						body += `${sBoundary}Content-Disposition: form-data; name=\"synchronous\"\r\n\r\ntrue\r\n`;
						body += `${sBoundary}Content-Disposition: form-data; name=\"files[]\"; filename=\"${file.name}\"\r\nContent-Type: image/jpg\r\n`;

						const eBoundary = '\r\n--' + boundary + '--' + '\r\n';
						const bodyArray = new TextEncoder().encode(body);
						const endBoundaryArray = new TextEncoder().encode(eBoundary);

						const formDataArray = new Uint8Array(bodyArray.length + imgfile.byteLength + endBoundaryArray.length);
						formDataArray.set(bodyArray, 0);
						formDataArray.set(new Uint8Array(imgfile), bodyArray.length);
						formDataArray.set(endBoundaryArray, bodyArray.length + imgfile.byteLength);

						const url = `${this.settings.baseUrl}/uploads.json`;
						const headers = {
							"Api-Key": this.settings.apiKey,
							"Api-Username": this.settings.disUser,
							"Content-Type": `multipart/form-data; boundary=${boundary}`
						};

						const response = await requestUrl({
							url: url,
							method: "POST",
							body: formDataArray,
							throw: false,
							headers: headers,
						});

						if (response.status == 200) {
							const jsonResponse = response.json();
							console.log(`Upload Image jsonResponse: ${JSON.stringify(jsonResponse)}`);
							imageUrls.push(jsonResponse.url);
						} else {
							new NotifyUser(this.app, `Error uploading image: ${response.status}`).open();
							console.error(`Error uploading image: ${JSON.stringify(response.json)}`);
						}

Aye !! Je l’ai fait !

	async uploadImages(imageReferences: string[]): Promise<string[]> {
		const imageUrls = [];
		for (const ref of imageReferences) {
			const filePath = this.app.metadataCache.getFirstLinkpathDest(ref, this.activeFile.name)?.path;
			if (filePath) {
				const file = this.app.vault.getAbstractFileByPath(filePath) as TFile;
				if (file) {
					try {
						const imgfile = await this.app.vault.readBinary(file);
						const boundary = genBoundary();
						const sBoundary = '--' + boundary + '\r\n';
						const imgForm = `${sBoundary}Content-Disposition: form-data; name=\"file\"; filename=\"${file.name}\"\r\nContent-Type: image/${file.extension}\r\n\r\n`;


						let body = '';
						body += `\r\n${sBoundary}Content-Disposition: form-data; name=\"type\"\r\n\r\ncomposer\r\n`;
						body += `${sBoundary}Content-Disposition: form-data; name=\"synchronous\"\r\n\r\ntrue\r\n`;

						const eBoundary = '\r\n--' + boundary + '--\\r\\n';
						const imgFormArray = new TextEncoder().encode(imgForm);
						const bodyArray = new TextEncoder().encode(body);
						const endBoundaryArray = new TextEncoder().encode(eBoundary);

						const formDataArray = new Uint8Array(imgFormArray.length + imgfile.byteLength + bodyArray.length + endBoundaryArray.length);
						formDataArray.set(imgFormArray, 0);
						formDataArray.set(new Uint8Array(imgfile), imgFormArray.length);
						formDataArray.set(bodyArray, imgFormArray.length + imgfile.byteLength);
						formDataArray.set(endBoundaryArray, imgFormArray.length + bodyArray.length + imgfile.byteLength);

						const url = `${this.settings.baseUrl}/uploads.json`;
						const headers = {
							"Api-Key": this.settings.apiKey,
							"Api-Username": this.settings.disUser,
							"Content-Type": `multipart/form-data; boundary=${boundary}`,
						};

						const response = await requestUrl({
							url: url,
							method: "POST",
							body: formDataArray.buffer,
							throw: false,
							headers: headers,
						});

						if (response.status == 200) {
							const jsonResponse = response.json;
							console.log(`Upload Image jsonResponse: ${JSON.stringify(jsonResponse)}`);
							imageUrls.push(jsonResponse.url);
						} else {
							new NotifyUser(this.app, `Error uploading image: ${response.status}`).open();
							console.error(`Error uploading image: ${JSON.stringify(response.json)}`);
						}
					} catch (error) {
						new NotifyUser(this.app, `Exception while uploading image: ${error}`).open();
						console.error("Exception while uploading image:", error);
					}
				} else {
					new NotifyUser(this.app, `File not found in vault: ${ref}`).open();
					console.error(`File not found in vault: ${ref}`);
				}
			} else {
				new NotifyUser(this.app, `Unable to resolve file path for: ${ref}`).open();
				console.error(`Unable to resolve file path for: ${ref}`);
			}
		}
		return imageUrls;
	}

Le problème était l’ordre dans lequel je construisais le form-data. Je devais avoir :

  • paramètres img
  • binaire img
  • paramètres

Auparavant, je mettais les paramètres avant l’image.
J’ai résolu cela en analysant un téléversement réussi en utilisant python :

import requests
from requests_toolbelt.multipart.encoder import MultipartEncoder
from requests.models import PreparedRequest


class Discourse:
    def __init__(self):
        self.base_url = "CENSORED"
        self.api_key = "CENSORED"
        self.api_username = "CENSORED"
        self.category = 2

    def post_uploads(self, file_path):
        headers = {
            "Content-Type": "multipart/form-data",
            "Api-Key": self.api_key,
            "Api-Username": self.api_username
        }

        multi = MultipartEncoder(
            fields = {
                'file': ('filename', open(file_path, 'rb'), 'image/jpg'),
                'type': 'composer',
                'synchronous': 'true'
            }
        )

        headers['Content-Type'] = multi.content_type

        request = requests.Request(
            method = "POST",
            url = f"{self.base_url}/uploads.json",
            headers = headers,
            data = multi
        )
        prepared_request = request.prepare()

        print("Headers:")
        for k, v in prepared_request.headers.items():
            print(f"{k}: {v}")

        print(multi.to_string())

        response = requests.post(
            f"{self.base_url}/uploads.json",
            headers=headers,
            params=params,
            data=m
        )

        return response.json()


if __name__ == "__main__":
    ds = Discourse()
    response = ds.post_uploads("/home/tfinley/Pictures/face2.jpg")
    print(response)

Maintenant, comment supprimer les téléversements orphelins ? ^_o

3 « J'aime »

D’après ce que je comprends, Discourse s’en chargera pour vous :

Oui, j’ai vu ça. GG EZ WP

Je suis en retard, mais j’ai déjà écrit une API discourse tierce pour nodejs :

Avec cette bibliothèque, vous pouvez facilement créer des téléchargements. Faites simplement ceci :

const { DiscourseApi } = require("node-discourse-api");
const api = new DiscourseApi("https://discourse.example.com");
api.options.api_username = "API_USERNAME";
api.options.api_key = "API_KEY";

api.createUpload(file_path_or_buffer, { filename: "filename" })

(Note : Cette bibliothèque n’est pas complète)

2 « J'aime »

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.