refactor(mobile): extract normalizeFileUri to shared utility module

Address code review feedback:
- Extract normalizeFileUri to mobile/src/utils/normalizeUri.ts
- Import shared function in ShareContext and UploadScreen
- Move os import to top of test file
- Update test docstring to reflect new behavior

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-19 14:11:08 +00:00
parent d5c18ccf07
commit ec882214e2
4 changed files with 25 additions and 44 deletions
+1 -22
View File
@@ -9,6 +9,7 @@
*/
import React, { createContext, useCallback, useContext, useState } from "react";
import { normalizeFileUri } from "../utils/normalizeUri";
export interface SharedFile {
uri: string;
@@ -28,28 +29,6 @@ const ShareContext = createContext<ShareContextValue>({
clearPendingFiles: () => {},
});
/**
* Aggressively normalise a file URI so that the same physical file is
* recognised regardless of how the URI was constructed.
*
* - Decode percent-encoding (`%20` → ` `)
* - Collapse consecutive slashes after the scheme (`file:////` → `file:///`)
* - Strip trailing slashes
*/
function normalizeFileUri(uri: string): string {
let norm: string;
try {
norm = decodeURIComponent(uri);
} catch {
norm = uri;
}
// Collapse multiple slashes after the scheme (e.g. file://// → file:///)
norm = norm.replace(/^(file:\/\/)\/{2,}/, "$1/");
// Strip trailing slash
norm = norm.replace(/\/+$/, "");
return norm;
}
export function ShareProvider({ children }: { children: React.ReactNode }) {
const [pendingFiles, setPendingFiles] = useState<SharedFile[]>([]);
+2 -20
View File
@@ -28,30 +28,12 @@ import {
} from "react-native";
import { useAuth } from "../context/AuthContext";
import { useShare } from "../context/ShareContext";
import { normalizeFileUri } from "../utils/normalizeUri";
import api from "../services/api";
/** Statuses that indicate processing has finished (no further polling needed). */
const TERMINAL_STATUSES = new Set(["completed", "failed", "duplicate"]);
/**
* Normalise a file URI for deduplication.
*
* - Decode percent-encoding (`%20` → ` `)
* - Collapse consecutive slashes after the scheme (`file:////` → `file:///`)
* - Strip trailing slashes
*/
function normalizeUri(uri: string): string {
let norm: string;
try {
norm = decodeURIComponent(uri);
} catch {
norm = uri;
}
norm = norm.replace(/^(file:\/\/)\/{2,}/, "$1/");
norm = norm.replace(/\/+$/, "");
return norm;
}
interface UploadItem {
id: string;
filename: string;
@@ -130,7 +112,7 @@ export default function UploadScreen() {
// Deduplicate: skip if this exact URI was already uploaded in this session.
// This guards against duplicate share-sheet deliveries from iOS where the
// Linking handler and +not-found.tsx fire for the same file.
const normUri = normalizeUri(uri);
const normUri = normalizeFileUri(uri);
if (uploadedUrisRef.current.has(normUri)) {
console.debug("[uploadFile] skipping duplicate URI:", uri);
return;
+20
View File
@@ -0,0 +1,20 @@
/**
* Normalise a file URI for deduplication.
*
* - Decode percent-encoding (`%20` → ` `)
* - Collapse consecutive slashes after the scheme (`file:////` → `file:///`)
* - Strip trailing slashes
*/
export function normalizeFileUri(uri: string): string {
let norm: string;
try {
norm = decodeURIComponent(uri);
} catch {
norm = uri;
}
// Collapse multiple slashes after the scheme (e.g. file://// → file:///)
norm = norm.replace(/^(file:\/\/)\/{2,}/, "$1/");
// Strip trailing slash
norm = norm.replace(/\/+$/, "");
return norm;
}
+2 -2
View File
@@ -3,11 +3,12 @@
Covers:
- ``GET /api/duplicates`` — list all exact-duplicate groups
- ``GET /api/files/{id}/duplicates`` — per-file exact + near-duplicate info
- ``POST /api/ui-upload`` — exact-duplicate warning in upload response
- ``POST /api/ui-upload`` — exact-duplicate rejection at upload time
- ``GET /duplicates`` — duplicate management UI page
"""
import json
import os
from unittest.mock import patch
import pytest
@@ -395,7 +396,6 @@ class TestUploadDuplicateRejection:
# The stored_filename is returned so we can verify cleanup
stored = data.get("stored_filename")
assert stored is not None
import os
from app.config import settings