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:
@@ -9,6 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createContext, useCallback, useContext, useState } from "react";
|
import React, { createContext, useCallback, useContext, useState } from "react";
|
||||||
|
import { normalizeFileUri } from "../utils/normalizeUri";
|
||||||
|
|
||||||
export interface SharedFile {
|
export interface SharedFile {
|
||||||
uri: string;
|
uri: string;
|
||||||
@@ -28,28 +29,6 @@ const ShareContext = createContext<ShareContextValue>({
|
|||||||
clearPendingFiles: () => {},
|
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 }) {
|
export function ShareProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [pendingFiles, setPendingFiles] = useState<SharedFile[]>([]);
|
const [pendingFiles, setPendingFiles] = useState<SharedFile[]>([]);
|
||||||
|
|
||||||
|
|||||||
@@ -28,30 +28,12 @@ import {
|
|||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useShare } from "../context/ShareContext";
|
import { useShare } from "../context/ShareContext";
|
||||||
|
import { normalizeFileUri } from "../utils/normalizeUri";
|
||||||
import api from "../services/api";
|
import api from "../services/api";
|
||||||
|
|
||||||
/** Statuses that indicate processing has finished (no further polling needed). */
|
/** Statuses that indicate processing has finished (no further polling needed). */
|
||||||
const TERMINAL_STATUSES = new Set(["completed", "failed", "duplicate"]);
|
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 {
|
interface UploadItem {
|
||||||
id: string;
|
id: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
@@ -130,7 +112,7 @@ export default function UploadScreen() {
|
|||||||
// Deduplicate: skip if this exact URI was already uploaded in this session.
|
// Deduplicate: skip if this exact URI was already uploaded in this session.
|
||||||
// This guards against duplicate share-sheet deliveries from iOS where the
|
// This guards against duplicate share-sheet deliveries from iOS where the
|
||||||
// Linking handler and +not-found.tsx fire for the same file.
|
// Linking handler and +not-found.tsx fire for the same file.
|
||||||
const normUri = normalizeUri(uri);
|
const normUri = normalizeFileUri(uri);
|
||||||
if (uploadedUrisRef.current.has(normUri)) {
|
if (uploadedUrisRef.current.has(normUri)) {
|
||||||
console.debug("[uploadFile] skipping duplicate URI:", uri);
|
console.debug("[uploadFile] skipping duplicate URI:", uri);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -3,11 +3,12 @@
|
|||||||
Covers:
|
Covers:
|
||||||
- ``GET /api/duplicates`` — list all exact-duplicate groups
|
- ``GET /api/duplicates`` — list all exact-duplicate groups
|
||||||
- ``GET /api/files/{id}/duplicates`` — per-file exact + near-duplicate info
|
- ``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
|
- ``GET /duplicates`` — duplicate management UI page
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -395,7 +396,6 @@ class TestUploadDuplicateRejection:
|
|||||||
# The stored_filename is returned so we can verify cleanup
|
# The stored_filename is returned so we can verify cleanup
|
||||||
stored = data.get("stored_filename")
|
stored = data.get("stored_filename")
|
||||||
assert stored is not None
|
assert stored is not None
|
||||||
import os
|
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user