Como postar fotos via API?

Estou trabalhando em um plugin para o Obsidian e tenho batido a cabeça tentando fazer o upload de imagens. Eis o que tenho até agora:

	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;
	}

Estou construindo um multipart/form-data porque requestURL() não pode aceitar um formData() como parâmetro. Apenas string ou arrayBuffer. Não posso usar fetch() pois recebo um erro CORS. Com este código (e muitas pequenas alterações no body), estou recebendo o seguinte erro:

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

1 curtida

Pensei em ir em frente e atualizar, pois agora estou recebendo uma mensagem de erro diferente:

	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;
	}

A mensagem de erro que estou recebendo agora é:

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

Isso é confuso para mim porque a API afirma que precisamos enviar um multipart/form-data, mas está dizendo JSON inválido? Talvez esteja relacionado a requestAPI().

Então pensei em tentar uma abordagem diferente usando o armazenamento S3. Segui as instruções aqui para configurar um bucket AWS S3. Aqui estão minhas configurações:

E aqui está meu código:

	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;
	}

Agora, percebo que isso atualmente não funcionará porque ainda não estou realmente carregando o arquivo. Pelo que li na documentação, eu enviaria um objeto json contendo o tipo, nome do arquivo e tamanho do arquivo. A API deve responder com uma chave e uma URL para eu usar na transferência real do arquivo. Mas neste ponto, estou recebendo o seguinte erro:

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

[
“The requested URL or resource could not be found.”
]

Verifiquei minha chave de API para ter certeza de que ela tinha permissões, ela tem. Mas criei uma nova de qualquer maneira. E uma global. Nenhuma delas está funcionando. Mesmo código de erro. O que estou fazendo de errado?

Editar, aqui está o objeto img:

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

Não estou muito familiarizado com esse ecossistema, mas talvez isso ajude?

Erro CORS…

Você seguiu Setup Cross-Origin Resource Sharing (CORS)?

1 curtida

Estou adivinhando um pouco aqui, mas não acho que configurar CORS funcionará para este caso. A origem (pelo menos do aplicativo Obsidian Desktop) é 'app://obsidian.md'. Eu acho que CORS só pode ser configurado no Discourse para lidar com requisições HTTP.

@maxtim, você precisa que isso funcione do celular, ou apenas poder postar no Discourse do aplicativo desktop seria o suficiente? Estou adivinhando um pouco novamente, mas… meu entendimento é que o aplicativo desktop é um aplicativo Electron. Ele está rodando em uma combinação de Chromium e Node.js. Você pode ser capaz de usar node-fetch para fazer requisições do lado do servidor para o Discourse. Se isso funcionar, isso resolverá o problema de CORS e permitirá que você use FormData nas requisições.

Fui em frente e tentei (proverbialmente jogando macarrão molhado para ver o que grudava). Mas eu já estava apreensivo e o @simon está correto.

Idealmente, o plugin estaria disponível no celular também. Mas, por enquanto, se apenas o Desktop for o que conseguirmos, é o que conseguiremos.

Claro, outra solução pode ser: Obsidian Vault no celular → sincronizar para Desktop → cli para fazer upload para o Discourse. Mas isso parece um pouco complicado.

Basicamente, a situação ideal é que o fórum Discourse substitua o Obsidian Vault. Dessa forma, os usuários que preferem o fórum podem usar o fórum. Os usuários que preferem (ou realmente precisam de uma solução offline) podem usar o Obsidian. Já tenho algumas ideias sobre como uma sincronização bidirecional pode funcionar. Mas acho que imagens/arquivos precisam ser tratados de alguma forma primeiro.

Editar:
Estou bastante convencido de que isso funcionará, mas não consigo definir os parâmetros corretamente:


						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!! Eu consegui!

	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;
	}

O problema era a ordem em que eu estava construindo o form-data. Eu precisava que fosse:

  • parâmetros da imagem
  • binário da imagem
  • parâmetros

Eu estava colocando os parâmetros antes da imagem anteriormente.
Eu resolvi isso analisando um upload bem-sucedido usando 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)

Agora, como eu deleto uploads órfãos? ^_o

3 curtidas

Pelo que entendi, o Discourse cuidará disso para você:

Sim, vi isso. GG EZ WP

Cheguei atrasado, mas já escrevi uma API de terceiros para o Discourse em nodejs antes:

Com esta biblioteca você pode facilmente criar uploads. Basta fazer o seguinte:

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" })

(Observação: esta biblioteca não está completa)

2 curtidas

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