Skip to content

Conversation

@Doezer
Copy link
Owner

@Doezer Doezer commented Jan 31, 2026

This pull request introduces several new components and features to enhance the import workflow in the application. The main changes add user interfaces for reviewing imports, configuring import settings, and browsing the file system, as well as integrating these features into the dashboard.

New Import Workflow Components and Settings:

  • Added a comprehensive import review modal, allowing users to review and confirm imports, select destination paths, choose import strategies, and decide whether to delete source files. This modal uses the new file browser component for path selection.
  • Introduced an import settings page with tabs for configuring general import options, RomM integration, path mappings, and platform mappings, including support for saving settings and live feedback.

File System Browsing:

  • Added a reusable FileBrowser component that provides a dialog-based interface for navigating directories, selecting folders, and handling loading/error states. This is used within the import review modal for destination selection.

Dashboard Integration:

  • Integrated a new PendingImportsCard into the dashboard to display pending imports, supporting the new workflow. [1] [2]

@Doezer Doezer self-assigned this Jan 31, 2026
@Doezer Doezer added this to the 1.2.0 milestone Jan 31, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @Doezer, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the application's import capabilities by introducing a comprehensive new workflow for post-processing completed downloads. It provides users with granular control over how downloaded content is handled, including options for automatic unpacking, renaming, and integration with external tools like RomM. The changes aim to streamline the process of adding new games to a library, offering both automated and manual review options to ensure content is correctly organized.

Highlights

  • Comprehensive Import Workflow: Introduces new UI components for reviewing imports, configuring settings (general, RomM, path/platform mappings), and browsing the file system, streamlining post-processing of completed downloads.
  • RomM Integration: Adds dedicated settings and backend logic to integrate with RomM (Rom Manager) for organizing game ROMs, including platform mapping and triggering library scans.
  • Robust Post-Processing Engine: Implements a new backend import manager with services for path translation, archive extraction (7-Zip), and strategy-based import execution (PC/RomM), handling both automated and manual review scenarios.
  • Dynamic File Browser: A new reusable FileBrowser component allows users to interactively navigate directories and select paths within the application, enhancing user experience for path configuration.
  • Database Schema Enhancements: New tables (path_mappings, platform_mappings) and extended user_settings fields are added to persist the new import and RomM configurations.
  • Dashboard Integration: A PendingImportsCard is added to the dashboard to alert users to downloads requiring manual import review, providing a quick access point for action.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a major new feature for post-processing downloads, including RomM integration. It adds several new UI components for managing import settings, path/platform mappings, and reviewing pending imports. The backend is significantly extended with new services for handling the import logic, archive extraction, and file system interaction. The routing has been refactored for better organization.

Overall, this is a substantial and well-structured feature addition. However, there are some critical issues that need to be addressed. There's a database migration conflict that will prevent the application from starting correctly. The manual import confirmation flow is currently broken due to an issue with how file paths are handled between the frontend and backend. There's also a potential path traversal vulnerability in the new file browser API.

I've left several comments with suggestions on how to fix these issues. Addressing them will make this new feature robust and secure.

@@ -0,0 +1,112 @@

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

There appears to be a migration conflict. This file and 0002_careful_katie_power.sql share the same migration version number (0002). This typically occurs when multiple developers generate migrations from the same base and can lead to an inconsistent database schema. To resolve this, please rebase your branch and regenerate the migrations to create a single, sequential set of migration files.

Comment on lines +168 to +223
async confirmImport(downloadId: string, overridePlan?: ImportReview & { deleteSource?: boolean }): Promise<void> {
const download = await this.storage.getGameDownload(downloadId);

if (!download) {
throw new Error(`Download ${downloadId} not found`);
}

// HACK: For now, I'll trust that I can fetch it.
// I'll implement a proper fetch or use a query if I could.
// Let's assume passed overridePlan is sufficient to Execute?
// No, we need DB record to update status.

if (!overridePlan) {
throw new Error("Confirmation requires a plan");
}

// Execute
// We assume the user validated the plan.
// Re-instantiate strategy?
let _strategy: ImportStrategy;
if (overridePlan.strategy === 'romm') {
// We need platform slug. It should be in the plan or we derive?
// Taking it from plan would be unsafe if not validated.
// But platformSlug is private in RomMStrategy.
// Let's create a generic "ManualStrategy" or reuse RomM/PC.
// If strategy is 'romm', we need the slug. import the file to `data/roms/{slug}`.
// Actually, the plan has `proposedPath`. We just move to `proposedPath`.
// We don't strictly need the Strategy instance if `executeImport` just does file moves.
// But `executeImport` calls `fs.move`.

// Let's just run the move logic directly here or via a GenericStrategy.
// eslint-disable-next-line no-console
console.log(`[ImportManager] Executing manual import to ${overridePlan.proposedPath}`);
await fs.ensureDir(path.dirname(overridePlan.proposedPath));
// Atomic move logic
const tempDest = overridePlan.proposedPath + ".tmp";
await fs.move(overridePlan.originalPath, tempDest, { overwrite: true });
await fs.move(tempDest, overridePlan.proposedPath, { overwrite: true });

if (overridePlan.deleteSource) {
// Delete source logic
}
} else {
// PC Strategy
await fs.ensureDir(path.dirname(overridePlan.proposedPath));
await fs.move(overridePlan.originalPath, overridePlan.proposedPath, { overwrite: true });
}

const downloadStatus = "imported";
await this.storage.updateGameDownloadStatus(downloadId, downloadStatus);

// Update Game Status
// We need gameId. Retrieve from download record.
// If we cant fetch download record, we can't update game.
// TODO: Add `getGameDownload(id)` to IStorage.
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The confirmImport logic has a critical flaw that will prevent manual import confirmations from working. It relies on overridePlan.originalPath, but as noted in the frontend code, this path is not available and an empty string is sent, which will cause fs.move() to fail. Additionally, this method duplicates file-moving logic from the ImportStrategy classes, making it brittle. To fix this, I recommend the following: 1. Ensure originalPath is available. You could either store the remotePath on the GameDownload entity when review is required, or have the backend re-fetch the download details from the downloader to get the path. 2. Refactor this method to reuse the ImportStrategy implementations. You can instantiate the appropriate strategy based on overridePlan.strategy and call its executeImport method with the confirmed plan, avoiding code duplication.

Comment on lines +52 to +75
originalPath: "", // Backend should know or we don't have it easily.
// Actually the backend endpoint I wrote requires `originalPath`.
// Wait, `ImportManager.confirmImport` requires `overridePlan`.
// The backend `POST` handler constructs the plan.
// But `ImportManager.confirmImport` signature:
// async confirmImport(downloadId: string, overridePlan?: ImportReview & { deleteSource?: boolean }): Promise<void>
// The route handler I wrote:
// body matches { strategy, originalPath, proposedPath, deleteSource }
// Request body schema: z.object({ strategy, originalPath, proposedPath, deleteSource ... })
// I made `originalPath` required in the Zod schema in `server/routes/import.ts`.
// This is a problem if I don't know the original path here.
// The `GET /api/imports/pending` endpoint returns `GameDownload` fields but NOT the remote path explicitly unless I add it.
// I should update `GET /api/imports/pending` to include `remotePath` if possible,
// OR update `POST` to make `originalPath` optional (ImportManager can try to find it from download details?).
// Actually `ImportManager.confirmImport` doesn't strictly need `originalPath` if it's just moving FROM somewhere.
// But `ImportManager` logic usually moves FROM `originalPath` (the extract output or download dir).
// If I select "PC Strategy", it moves FROM `source` TO `dest`.
// If the download is "completed_pending_review", the files are sitting in the download folder.
// `ImportManager` usually knows this path via `DownloaderManager`.
// So I should arguably make `originalPath` optional in the API and let backend resolve it if missing.
// Let's assume for now I will fix the backend to make `originalPath` optional or resolve it.
// For now I'll send an empty string and hope the backend handles it or I'll fix the backend.
// Actually, I'll send a placeholder "." and fix the backend to ignore it if it trusts its own lookup.
deleteSource
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

These extensive comments appear to be development notes and should be removed from the final code. More importantly, they highlight a significant issue: originalPath is required by the backend but is not available on the frontend, leading to an empty string being sent. This will cause the import confirmation to fail.

To resolve this, the backend should be the source of truth. Consider one of the following approaches:

  1. The GET /api/imports/pending endpoint could be updated to include the originalPath for each pending item.
  2. The POST /api/imports/:id/confirm endpoint could be modified to not require originalPath from the client, and instead resolve it on the server-side using the download's hash or other stored information.

Comment on lines +10 to +13
const rawPath = req.query.path as string || "/";
// Sanitize? We generally trust admin user but good to prevent escaping root of container if intended.
// Docker container root access is usually implied for this feature.
const validPath = path.resolve(rawPath);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The file browser endpoint is vulnerable to path traversal. path.resolve(rawPath) on its own does not restrict file system access to a base directory. An attacker could potentially access sensitive files outside the intended media folder (e.g., by using a path like ../../../../etc/passwd).

Even within a Docker container, this can be a security risk. It's crucial to sandbox file browsing to a specific, configurable root directory.

Here's an example of how you could enforce this sandboxing:

const root = process.env.MEDIA_ROOT || '/data'; // Or another configurable base path
const userPath = path.normalize(rawPath);
const safePath = path.join(root, userPath);

if (!safePath.startsWith(root)) {
  return res.status(400).json({ error: 'Invalid path: traversal detected' });
}

// Proceed with the sanitized `safePath`

Comment on lines +59 to +61
onSuccess: (data: any) => { // eslint-disable-line
toast({ title: "Defaults Initialized", description: `Added ${data.count || 0} default mappings.` });
alert(`Initialized ${data.count || 0} defaults.`); // Explicit alert for verification
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of alert() here is inconsistent with the rest of the application, which uses toast notifications for user feedback. The comment // Explicit alert for verification suggests this may have been for debugging. Please replace the alert() with a toast to provide a more consistent and less intrusive user experience. Additionally, using data: any should be avoided; a more specific type should be used for the response data.

Suggested change
onSuccess: (data: any) => { // eslint-disable-line
toast({ title: "Defaults Initialized", description: `Added ${data.count || 0} default mappings.` });
alert(`Initialized ${data.count || 0} defaults.`); // Explicit alert for verification
onSuccess: (data: { count?: number }) => {
toast({ title: "Defaults Initialized", description: `Added ${data.count || 0} default mappings.` });

`id` text PRIMARY KEY NOT NULL,
`remote_path` text NOT NULL,
`local_path` text NOT NULL,
`remote_host` text NOT NULL,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The remote_host column is defined as NOT NULL, but it should be nullable to allow for generic path mappings that apply to any host. This is corrected in a subsequent migration (0003_colorful_ben_grimm.sql), but it's best practice for each migration to be correct on its own to avoid issues if migrations are run incrementally. Please consider altering this migration to define remote_host as nullable from the start.

// For now, I'll assume `/data` or derive from mapping.
// Let's fallback to `/data` if not configured.
// TODO: Add `libraryRoot` to settings later.
const libraryRoot = "/data";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The libraryRoot is hardcoded to /data. As the TODO comment indicates, this should be a configurable setting to provide users with more flexibility for their directory structures. Please consider moving this to a configuration file or making it a database setting.

Suggested change
const libraryRoot = "/data";
const libraryRoot = config.libraryRoot || "/data";

@Doezer Doezer linked an issue Jan 31, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Emulator's ROMs support

1 participant