mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(upload): resolve electron file-path fallback and android provider automation stalls
This commit is contained in:
@@ -49,6 +49,8 @@ public class MediaUploadAutomationRunner {
|
|||||||
private final Handler mainHandler;
|
private final Handler mainHandler;
|
||||||
private final Runnable pollRunnable;
|
private final Runnable pollRunnable;
|
||||||
private boolean finished;
|
private boolean finished;
|
||||||
|
private boolean fileChooserHandled;
|
||||||
|
private boolean fileInputTriggerAttempted;
|
||||||
private long startTime;
|
private long startTime;
|
||||||
|
|
||||||
public MediaUploadAutomationRunner(
|
public MediaUploadAutomationRunner(
|
||||||
@@ -86,6 +88,7 @@ public class MediaUploadAutomationRunner {
|
|||||||
WebView webView,
|
WebView webView,
|
||||||
ValueCallback<Uri[]> filePathCallback,
|
ValueCallback<Uri[]> filePathCallback,
|
||||||
FileChooserParams fileChooserParams) {
|
FileChooserParams fileChooserParams) {
|
||||||
|
fileChooserHandled = true;
|
||||||
MediaUploadAutomationRunner.this.filePathCallback = filePathCallback;
|
MediaUploadAutomationRunner.this.filePathCallback = filePathCallback;
|
||||||
if (fileUri != null) {
|
if (fileUri != null) {
|
||||||
filePathCallback.onReceiveValue(new Uri[] {fileUri});
|
filePathCallback.onReceiveValue(new Uri[] {fileUri});
|
||||||
@@ -120,7 +123,21 @@ public class MediaUploadAutomationRunner {
|
|||||||
finish(new MediaUploadResult(false, null, "No trigger JS for " + provider));
|
finish(new MediaUploadResult(false, null, "No trigger JS for " + provider));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
webView.evaluateJavascript(js, value -> schedulePoll());
|
fileInputTriggerAttempted = true;
|
||||||
|
webView.evaluateJavascript(
|
||||||
|
js,
|
||||||
|
value -> {
|
||||||
|
String normalized = value == null ? "" : value.replace("\"", "").trim();
|
||||||
|
if ("true".equals(normalized)) {
|
||||||
|
schedulePoll();
|
||||||
|
} else {
|
||||||
|
finish(
|
||||||
|
new MediaUploadResult(
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
"Could not find upload file input for provider " + provider));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void schedulePoll() {
|
private void schedulePoll() {
|
||||||
@@ -136,6 +153,14 @@ public class MediaUploadAutomationRunner {
|
|||||||
finish(new MediaUploadResult(false, null, "Upload timeout"));
|
finish(new MediaUploadResult(false, null, "Upload timeout"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (fileInputTriggerAttempted
|
||||||
|
&& !fileChooserHandled
|
||||||
|
&& elapsed >= MediaUploadRecipes.FILE_INPUT_TIMEOUT_MS) {
|
||||||
|
finish(
|
||||||
|
new MediaUploadResult(
|
||||||
|
false, null, "Provider file chooser was not triggered for " + provider));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String successJs = MediaUploadRecipes.getSuccessJs(provider);
|
String successJs = MediaUploadRecipes.getSuccessJs(provider);
|
||||||
String blockedJs = MediaUploadRecipes.getBlockedJs(provider);
|
String blockedJs = MediaUploadRecipes.getBlockedJs(provider);
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ public final class MediaUploadRecipes {
|
|||||||
|
|
||||||
/** Max time to wait for upload completion (ms). */
|
/** Max time to wait for upload completion (ms). */
|
||||||
public static final long UPLOAD_TIMEOUT_MS = 45_000;
|
public static final long UPLOAD_TIMEOUT_MS = 45_000;
|
||||||
|
/** Max time to wait for provider file input to be found/triggered (ms). */
|
||||||
|
public static final long FILE_INPUT_TIMEOUT_MS = 8_000;
|
||||||
/** Poll interval for success/blocked checks (ms). */
|
/** Poll interval for success/blocked checks (ms). */
|
||||||
public static final long POLL_INTERVAL_MS = 500;
|
public static final long POLL_INTERVAL_MS = 500;
|
||||||
|
|
||||||
@@ -61,7 +63,7 @@ public final class MediaUploadRecipes {
|
|||||||
if (i > 0) sb.append(",");
|
if (i > 0) sb.append(",");
|
||||||
sb.append("\"").append(escapeJs(selectors[i])).append("\"");
|
sb.append("\"").append(escapeJs(selectors[i])).append("\"");
|
||||||
}
|
}
|
||||||
sb.append("];for(var i=0;i<s.length;i++){var el=document.querySelector(s[i]);if(el){el.click();return;}}})();");
|
sb.append("];for(var i=0;i<s.length;i++){var el=document.querySelector(s[i]);if(el){el.click();return true;}}return false;})()");
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { contextBridge, ipcRenderer } from 'electron';
|
import { contextBridge, ipcRenderer, webUtils } from 'electron';
|
||||||
|
|
||||||
// dev uses http://localhost, prod uses file://...index.html
|
// dev uses http://localhost, prod uses file://...index.html
|
||||||
const isDev = window.location.protocol === 'http:';
|
const isDev = window.location.protocol === 'http:';
|
||||||
@@ -21,4 +21,11 @@ contextBridge.exposeInMainWorld('electronApi', {
|
|||||||
copyToClipboard: (text) => ipcRenderer.invoke('copy-to-clipboard', text),
|
copyToClipboard: (text) => ipcRenderer.invoke('copy-to-clipboard', text),
|
||||||
getPlatform: () => ipcRenderer.invoke('get-platform'),
|
getPlatform: () => ipcRenderer.invoke('get-platform'),
|
||||||
automateUploadMedia: (options) => ipcRenderer.invoke('automate-upload-media', options),
|
automateUploadMedia: (options) => ipcRenderer.invoke('automate-upload-media', options),
|
||||||
|
getPathForFile: (file) => {
|
||||||
|
try {
|
||||||
|
return webUtils.getPathForFile(file);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Vendored
+1
@@ -13,6 +13,7 @@ declare global {
|
|||||||
copyToClipboard: (text: string) => Promise<{ success: boolean; error?: string }>;
|
copyToClipboard: (text: string) => Promise<{ success: boolean; error?: string }>;
|
||||||
getPlatform: () => Promise<{ platform: NodeJS.Platform; arch: string; version: string }>;
|
getPlatform: () => Promise<{ platform: NodeJS.Platform; arch: string; version: string }>;
|
||||||
automateUploadMedia: (options: { provider: ProviderId; filePath: string }) => Promise<{ url: string; provider: ProviderId }>;
|
automateUploadMedia: (options: { provider: ProviderId; filePath: string }) => Promise<{ url: string; provider: ProviderId }>;
|
||||||
|
getPathForFile?: (file: File) => string | null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { orchestrateElectronUpload } from '../upload-orchestrator';
|
||||||
|
import { uploadToCatbox } from '../../utils/catbox-utils';
|
||||||
|
|
||||||
|
vi.mock('../../utils/catbox-utils', () => ({
|
||||||
|
uploadToCatbox: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
function createElectronApiMock() {
|
||||||
|
return {
|
||||||
|
isElectron: true,
|
||||||
|
copyToClipboard: vi.fn(async () => ({ success: true })),
|
||||||
|
getPlatform: vi.fn(async () => ({ platform: 'darwin' as NodeJS.Platform, arch: 'x64', version: 'v20.0.0' })),
|
||||||
|
automateUploadMedia: vi.fn(async () => ({ url: 'https://i.imgur.com/abc.png', provider: 'imgur' as const })),
|
||||||
|
getPathForFile: vi.fn((): string | null => '/tmp/image.png'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('orchestrateElectronUpload', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
window.electronApi = undefined;
|
||||||
|
window.isElectron = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uploads via catbox provider directly', async () => {
|
||||||
|
vi.mocked(uploadToCatbox).mockResolvedValue('https://files.catbox.moe/a.png');
|
||||||
|
const file = new File(['a'], 'a.png', { type: 'image/png' });
|
||||||
|
|
||||||
|
const url = await orchestrateElectronUpload(file, ['catbox']);
|
||||||
|
|
||||||
|
expect(url).toBe('https://files.catbox.moe/a.png');
|
||||||
|
expect(uploadToCatbox).toHaveBeenCalledWith(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses electronApi.getPathForFile when File.path is unavailable', async () => {
|
||||||
|
const electronApi = createElectronApiMock();
|
||||||
|
window.electronApi = electronApi;
|
||||||
|
|
||||||
|
const file = new File(['x'], 'x.png', { type: 'image/png' });
|
||||||
|
const url = await orchestrateElectronUpload(file, ['imgur']);
|
||||||
|
|
||||||
|
expect(url).toBe('https://i.imgur.com/abc.png');
|
||||||
|
expect(electronApi.getPathForFile).toHaveBeenCalledWith(file);
|
||||||
|
expect(electronApi.automateUploadMedia).toHaveBeenCalledWith({
|
||||||
|
provider: 'imgur',
|
||||||
|
filePath: '/tmp/image.png',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails with provider attempt details if no file path can be resolved', async () => {
|
||||||
|
const electronApi = createElectronApiMock();
|
||||||
|
electronApi.getPathForFile = vi.fn((): string | null => null);
|
||||||
|
window.electronApi = electronApi;
|
||||||
|
|
||||||
|
const file = new File(['z'], 'z.png', { type: 'image/png' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await orchestrateElectronUpload(file, ['postimages']);
|
||||||
|
throw new Error('Expected orchestrateElectronUpload to throw');
|
||||||
|
} catch (error) {
|
||||||
|
const typedError = error as Error & {
|
||||||
|
attempts?: Array<{ provider: string; error?: string }>;
|
||||||
|
};
|
||||||
|
expect(typedError.message).toBe('All providers failed');
|
||||||
|
expect(typedError.attempts?.[0]?.provider).toBe('postimages');
|
||||||
|
expect(typedError.attempts?.[0]?.error).toContain('File path required for Electron automation');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,6 +8,23 @@ export interface OrchestratorAttempt {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveElectronFilePath(file: File): string | null {
|
||||||
|
const fileWithPath = file as File & { path?: string };
|
||||||
|
if (typeof fileWithPath.path === 'string' && fileWithPath.path.length > 0) {
|
||||||
|
return fileWithPath.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPathForFile = window.electronApi?.getPathForFile;
|
||||||
|
if (typeof getPathForFile === 'function') {
|
||||||
|
const resolvedPath = getPathForFile(file);
|
||||||
|
if (typeof resolvedPath === 'string' && resolvedPath.length > 0) {
|
||||||
|
return resolvedPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads a file via a single provider. Catbox uses the web API;
|
* Uploads a file via a single provider. Catbox uses the web API;
|
||||||
* imgur/postimages use Electron automation when available.
|
* imgur/postimages use Electron automation when available.
|
||||||
@@ -17,7 +34,7 @@ async function uploadViaProvider(provider: ProviderId, file: File): Promise<stri
|
|||||||
if (provider === 'imgur' || provider === 'postimages') {
|
if (provider === 'imgur' || provider === 'postimages') {
|
||||||
const fn = typeof window !== 'undefined' && window.electronApi?.automateUploadMedia;
|
const fn = typeof window !== 'undefined' && window.electronApi?.automateUploadMedia;
|
||||||
if (fn) {
|
if (fn) {
|
||||||
const filePath = (file as File & { path?: string }).path;
|
const filePath = resolveElectronFilePath(file);
|
||||||
if (!filePath) throw new Error('File path required for Electron automation');
|
if (!filePath) throw new Error('File path required for Electron automation');
|
||||||
const { url } = await fn({ provider, filePath });
|
const { url } = await fn({ provider, filePath });
|
||||||
return url;
|
return url;
|
||||||
|
|||||||
Reference in New Issue
Block a user