All files / src/lib/uploads storage.ts

58.82% Statements 10/17
76.47% Branches 13/17
100% Functions 0/0
58.82% Lines 10/17

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121                64x             18x                             18x                                       18x                     44x   40x       4x                       4x                       2x               4x                                          
import {
  type MultipartUploadContext,
  type MultipartUploadResumeState,
  RESUMABLE_UPLOAD_STORAGE_VERSION,
  type UploadFileFingerprint,
} from "./types";
 
function isBrowserStorageAvailable(): boolean {
  return (
    typeof window !== "undefined" && typeof window.localStorage !== "undefined"
  );
}
 
/** Build the stable browser-side fingerprint used to validate resume candidates. */
export function getFileFingerprint(file: File): UploadFileFingerprint {
  return {
    name: file.name,
    size: file.size,
    lastModified: file.lastModified,
    type: file.type,
  };
}
 
/** Create the persisted resume payload for an in-progress or already-uploaded CSV. */
export function createResumeState(
  context: MultipartUploadContext,
  file: File,
  uploadSessionId: string,
  uploadPhase: MultipartUploadResumeState["upload_phase"],
): MultipartUploadResumeState {
  return {
    version: RESUMABLE_UPLOAD_STORAGE_VERSION,
    upload_session_id: uploadSessionId,
    upload_phase: uploadPhase,
    file: getFileFingerprint(file),
    original_filename: file.name,
    content_type: file.type || "text/csv",
    ...context,
  };
}
 
/** Persist resumable upload state when browser storage is available. */
export function saveResumeState(
  key: string,
  state: MultipartUploadResumeState,
): void {
  if (!isBrowserStorageAvailable()) {
    return;
  }
 
  window.localStorage.setItem(key, JSON.stringify(state));
}
 
/** Load resumable upload state and reject anything that no longer matches the schema. */
export function loadResumeState(
  key: string,
): MultipartUploadResumeState | null {
  if (!isBrowserStorageAvailable()) {
    return null;
  }
 
  const rawValue = window.localStorage.getItem(key);
  if (!rawValue) {
    return null;
  }
 
  try {
    const parsed = JSON.parse(rawValue) as Partial<MultipartUploadResumeState>;
    if (
      parsed.version !== RESUMABLE_UPLOAD_STORAGE_VERSION ||
      typeof parsed.upload_session_id !== "string" ||
      !parsed.file ||
      typeof parsed.file.name !== "string" ||
      typeof parsed.file.size !== "number" ||
      typeof parsed.file.lastModified !== "number"
    ) {
      return null;
    }
 
    return parsed as MultipartUploadResumeState;
  } catch {
    return null;
  }
}
 
/** Remove the persisted resume payload for the provided upload key. */
export function clearResumeState(key: string): void {
  if (!isBrowserStorageAvailable()) {
    return;
  }
 
  window.localStorage.removeItem(key);
}
 
/** Ensure persisted resume state still belongs to the active flowsheet/upload context. */
export function resumeStateMatchesContext(
  state: MultipartUploadResumeState,
  context: MultipartUploadContext,
): boolean {
  return (
    state.purpose === context.purpose &&
    state.flowsheet_id === context.flowsheet_id &&
    state.scenario_id === context.scenario_id &&
    state.simulationObject_id === context.simulationObject_id
  );
}
 
/** Ensure the user re-selected the exact same local file before resuming an upload. */
export function resumeStateMatchesFile(
  state: MultipartUploadResumeState,
  file: File,
): boolean {
  const fingerprint = getFileFingerprint(file);
 
  return (
    state.file.name === fingerprint.name &&
    state.file.size === fingerprint.size &&
    state.file.lastModified === fingerprint.lastModified
  );
}