mirror of
https://github.com/immich-app/immich.git
synced 2026-06-04 22:05:19 -04:00
Compare commits
111 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f633612427 | |||
| 1d713f4829 | |||
| 0a587bd48d | |||
| 253143ed49 | |||
| f7780883c4 | |||
| 901d544efd | |||
| c07e87a7e5 | |||
| 5133ff1909 | |||
| 0c7f2dd8d2 | |||
| b09ebb11e9 | |||
| 181b028b09 | |||
| eb20b715e4 | |||
| a277c6311f | |||
| 5889c42eb6 | |||
| 14cce0cba3 | |||
| 9b80ffd9c6 | |||
| 306a3b8c7f | |||
| be0fc403d8 | |||
| c13fd9e4b5 | |||
| 8724848fce | |||
| 2d950db940 | |||
| 4b9ebc2cff | |||
| e2d26ebdea | |||
| 8c6adf7157 | |||
| 48fdd39d30 | |||
| 22bf7c2005 | |||
| 47b45453c8 | |||
| 448c069fb6 | |||
| 958f270f0d | |||
| 9f699fdfc3 | |||
| 00da7b88a1 | |||
| 144a57ddff | |||
| 1bd2d474d7 | |||
| b33874ef12 | |||
| dbaf4b548b | |||
| 7d58d5be12 | |||
| 42fe86d24c | |||
| eeb55c279b | |||
| 5c159d70a7 | |||
| 44ae0fa7ed | |||
| f782782662 | |||
| 4436cab827 | |||
| 74789ad1c4 | |||
| 7877097b3f | |||
| fb84c1cf61 | |||
| 940a1d4ab8 | |||
| fae25dbe65 | |||
| 8dd0d7f34c | |||
| 9b78f2c0ba | |||
| 67cedfef17 | |||
| c9c2322b9d | |||
| 389356149a | |||
| 4812a2e2d8 | |||
| 8f01d06927 | |||
| a2ff075e9a | |||
| d8b39906f9 | |||
| b36911a16b | |||
| b074ee202e | |||
| 78bb6cf926 | |||
| c980f5fc19 | |||
| a26d9e05ba | |||
| c862163204 | |||
| 5fb8f9bf1a | |||
| b9b5dba037 | |||
| 8bfa75087c | |||
| 95280edd6c | |||
| a9666d2cef | |||
| 4af9edc20b | |||
| 7ba458668b | |||
| ea034f21bc | |||
| a68513247d | |||
| 59f7f3c23e | |||
| c88bde3cab | |||
| 818bd51036 | |||
| 3c72409712 | |||
| 8d1a8b9465 | |||
| d880e7baed | |||
| 42801ace35 | |||
| 838b8e9126 | |||
| 9da5a48bdd | |||
| 27f126bd58 | |||
| a238c6a70d | |||
| 7222d7af30 | |||
| d660ab2218 | |||
| 69ffbcd5cf | |||
| bc84486668 | |||
| 2666ee2b4f | |||
| 72ea7799c0 | |||
| 98c8c28b62 | |||
| 6b1d26d3a2 | |||
| 5e07976288 | |||
| 3f1133f9b7 | |||
| 3a087ed2cd | |||
| c723a9ac78 | |||
| 550460891d | |||
| e3e8da168f | |||
| de117ebe7a | |||
| 3d507015e0 | |||
| fe71662d24 | |||
| 81a66350f6 | |||
| c33e65362a | |||
| bb5519036a | |||
| 177c997d96 | |||
| 2d6a2dc77b | |||
| e193cb3a5b | |||
| 4b63d3d055 | |||
| 4ed92f5df5 | |||
| 6f61bf04e4 | |||
| b21d0a1c53 | |||
| f80326872e | |||
| 7561c5e1c4 |
+1
-1
@@ -1 +1 @@
|
||||
24.13.1
|
||||
24.14.0
|
||||
|
||||
@@ -1,395 +0,0 @@
|
||||
import { readFileSync, appendFileSync, readdirSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
json: { type: "string" },
|
||||
name: { type: "string" },
|
||||
framework: { type: "string", default: "vitest" },
|
||||
coverage: { type: "string" },
|
||||
"pr-comment": { type: "boolean", default: false },
|
||||
"artifacts-dir": { type: "string" },
|
||||
},
|
||||
});
|
||||
|
||||
function readJson(path) {
|
||||
try {
|
||||
return JSON.parse(readFileSync(path, "utf8"));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function formatDuration(milliseconds) {
|
||||
if (milliseconds < 1000) {
|
||||
return `${milliseconds}ms`;
|
||||
}
|
||||
const seconds = milliseconds / 1000;
|
||||
if (seconds < 60) {
|
||||
return `${seconds.toFixed(1)}s`;
|
||||
}
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = (seconds % 60).toFixed(0);
|
||||
return `${minutes}m ${remainingSeconds}s`;
|
||||
}
|
||||
|
||||
function parseVitestResults(data) {
|
||||
const startTime = data.startTime ?? 0;
|
||||
const endTime = Math.max(
|
||||
...(data.testResults ?? []).map((r) => r.endTime ?? 0),
|
||||
startTime,
|
||||
);
|
||||
|
||||
return {
|
||||
total: data.numTotalTests ?? 0,
|
||||
passed: data.numPassedTests ?? 0,
|
||||
failed: data.numFailedTests ?? 0,
|
||||
skipped: data.numPendingTests ?? 0,
|
||||
flaky: 0,
|
||||
duration: endTime - startTime,
|
||||
success: data.success ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
function parsePlaywrightResults(data) {
|
||||
const stats = data.stats ?? {};
|
||||
const passed = stats.expected ?? 0;
|
||||
const failed = stats.unexpected ?? 0;
|
||||
const flaky = stats.flaky ?? 0;
|
||||
const skipped = stats.skipped ?? 0;
|
||||
|
||||
return {
|
||||
total: passed + failed + flaky + skipped,
|
||||
passed,
|
||||
failed,
|
||||
skipped,
|
||||
flaky,
|
||||
duration: stats.duration ?? 0,
|
||||
success: failed === 0,
|
||||
};
|
||||
}
|
||||
|
||||
function parseCoverageSummary(data) {
|
||||
const total = data.total ?? {};
|
||||
|
||||
const files = [];
|
||||
for (const [filePath, entry] of Object.entries(data)) {
|
||||
if (filePath === "total") {
|
||||
continue;
|
||||
}
|
||||
files.push({
|
||||
file: filePath.replace(/^.*?\/src\//, "src/"),
|
||||
lines: entry.lines?.pct ?? 0,
|
||||
branches: entry.branches?.pct ?? 0,
|
||||
functions: entry.functions?.pct ?? 0,
|
||||
statements: entry.statements?.pct ?? 0,
|
||||
});
|
||||
}
|
||||
files.sort((a, b) => a.lines - b.lines);
|
||||
|
||||
return {
|
||||
lines: total.lines?.pct ?? 0,
|
||||
branches: total.branches?.pct ?? 0,
|
||||
functions: total.functions?.pct ?? 0,
|
||||
statements: total.statements?.pct ?? 0,
|
||||
files,
|
||||
};
|
||||
}
|
||||
|
||||
function buildMarkdown(name, results, coverage) {
|
||||
const statusIcon =
|
||||
results.failed > 0
|
||||
? "\u274c"
|
||||
: results.flaky > 0
|
||||
? "\u26a0\ufe0f"
|
||||
: "\u2705";
|
||||
const lines = [];
|
||||
|
||||
lines.push(`### ${statusIcon} ${name}`);
|
||||
lines.push("");
|
||||
lines.push("| Metric | Value |");
|
||||
lines.push("|--------|-------|");
|
||||
lines.push(`| Total | ${results.total} |`);
|
||||
lines.push(`| Passed | ${results.passed} |`);
|
||||
lines.push(`| Failed | ${results.failed} |`);
|
||||
lines.push(`| Skipped | ${results.skipped} |`);
|
||||
if (results.flaky > 0) {
|
||||
lines.push(`| Flaky | ${results.flaky} |`);
|
||||
}
|
||||
lines.push(`| Duration | ${formatDuration(results.duration)} |`);
|
||||
lines.push("");
|
||||
|
||||
if (coverage) {
|
||||
lines.push("#### Coverage");
|
||||
lines.push("");
|
||||
lines.push("| Metric | Coverage |");
|
||||
lines.push("|--------|----------|");
|
||||
lines.push(`| Lines | ${coverage.lines}% |`);
|
||||
lines.push(`| Branches | ${coverage.branches}% |`);
|
||||
lines.push(`| Functions | ${coverage.functions}% |`);
|
||||
lines.push(`| Statements | ${coverage.statements}% |`);
|
||||
lines.push("");
|
||||
|
||||
if (coverage.files?.length > 0) {
|
||||
lines.push("<details>");
|
||||
lines.push(
|
||||
`<summary>File coverage (${coverage.files.length} files)</summary>`,
|
||||
);
|
||||
lines.push("");
|
||||
lines.push("| File | Lines | Branches | Functions |");
|
||||
lines.push("|------|-------|----------|-----------|");
|
||||
for (const file of coverage.files) {
|
||||
lines.push(
|
||||
`| ${file.file} | ${file.lines}% | ${file.branches}% | ${file.functions}% |`,
|
||||
);
|
||||
}
|
||||
lines.push("");
|
||||
lines.push("</details>");
|
||||
lines.push("");
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
const ARTIFACT_CONFIGS = [
|
||||
{
|
||||
pattern: "report-server-unit",
|
||||
name: "Server Unit Tests",
|
||||
framework: "vitest",
|
||||
testFile: "test-results.json",
|
||||
coverageFile: "coverage/coverage-summary.json",
|
||||
},
|
||||
{
|
||||
pattern: "report-web-unit",
|
||||
name: "Web Unit Tests",
|
||||
framework: "vitest",
|
||||
testFile: "test-results.json",
|
||||
coverageFile: "coverage/coverage-summary.json",
|
||||
},
|
||||
{
|
||||
pattern: "report-server-medium",
|
||||
name: "Server Medium Tests",
|
||||
framework: "vitest",
|
||||
testFile: "test-results-medium.json",
|
||||
coverageFile: "coverage/coverage-summary.json",
|
||||
},
|
||||
{
|
||||
pattern: "report-cli-unit",
|
||||
name: "CLI Unit Tests",
|
||||
framework: "vitest",
|
||||
testFile: "test-results.json",
|
||||
coverageFile: undefined,
|
||||
},
|
||||
{
|
||||
pattern: "report-cli-unit-win",
|
||||
name: "CLI Unit Tests (Windows)",
|
||||
framework: "vitest",
|
||||
testFile: "test-results.json",
|
||||
coverageFile: undefined,
|
||||
},
|
||||
{
|
||||
pattern: "report-e2e-server-cli-",
|
||||
name: "E2E Server & CLI",
|
||||
framework: "vitest",
|
||||
testFile: "test-results.json",
|
||||
coverageFile: undefined,
|
||||
},
|
||||
{
|
||||
pattern: "report-e2e-server-maintenance-",
|
||||
name: "E2E Server Maintenance",
|
||||
framework: "vitest",
|
||||
testFile: "test-results.json",
|
||||
coverageFile: undefined,
|
||||
},
|
||||
{
|
||||
pattern: "report-e2e-web-",
|
||||
name: "E2E Web",
|
||||
framework: "playwright",
|
||||
testFile: "test-results.json",
|
||||
coverageFile: undefined,
|
||||
},
|
||||
{
|
||||
pattern: "report-e2e-web-ui-",
|
||||
name: "E2E Web UI",
|
||||
framework: "playwright",
|
||||
testFile: "test-results.json",
|
||||
coverageFile: undefined,
|
||||
},
|
||||
{
|
||||
pattern: "report-e2e-web-maintenance-",
|
||||
name: "E2E Web Maintenance",
|
||||
framework: "playwright",
|
||||
testFile: "test-results.json",
|
||||
coverageFile: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
function getStatusIcon(results) {
|
||||
if (results.failed > 0) {
|
||||
return "\u274c";
|
||||
}
|
||||
if (results.flaky > 0) {
|
||||
return "\u26a0\ufe0f";
|
||||
}
|
||||
return "\u2705";
|
||||
}
|
||||
|
||||
function discoverArtifacts(artifactsDir) {
|
||||
const dirs = readdirSync(artifactsDir, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory())
|
||||
.map((d) => d.name);
|
||||
|
||||
const suites = [];
|
||||
|
||||
for (const dir of dirs) {
|
||||
const config = ARTIFACT_CONFIGS.find((c) => dir.startsWith(c.pattern));
|
||||
if (!config) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const suffix = dir.slice(config.pattern.length);
|
||||
const displayName = suffix ? `${config.name} (${suffix})` : config.name;
|
||||
|
||||
const testFilePath = join(artifactsDir, dir, config.testFile);
|
||||
const testData = readJson(testFilePath);
|
||||
if (!testData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const results =
|
||||
config.framework === "playwright"
|
||||
? parsePlaywrightResults(testData)
|
||||
: parseVitestResults(testData);
|
||||
|
||||
let coverage;
|
||||
if (config.coverageFile) {
|
||||
const coveragePath = join(artifactsDir, dir, config.coverageFile);
|
||||
const coverageData = readJson(coveragePath);
|
||||
if (coverageData) {
|
||||
coverage = parseCoverageSummary(coverageData);
|
||||
}
|
||||
}
|
||||
|
||||
suites.push({ name: displayName, results, coverage });
|
||||
}
|
||||
|
||||
return suites;
|
||||
}
|
||||
|
||||
function buildPrComment(suites) {
|
||||
if (suites.length === 0) {
|
||||
return "## Test Report\n\nNo test results found.\n";
|
||||
}
|
||||
|
||||
const lines = [];
|
||||
const totalFailed = suites.reduce((s, r) => s + r.results.failed, 0);
|
||||
const totalFlaky = suites.reduce((s, r) => s + r.results.flaky, 0);
|
||||
const overallIcon =
|
||||
totalFailed > 0 ? "\u274c" : totalFlaky > 0 ? "\u26a0\ufe0f" : "\u2705";
|
||||
|
||||
lines.push(`## ${overallIcon} Test Report`);
|
||||
lines.push("");
|
||||
lines.push("| Suite | Tests | Passed | Failed | Skipped | Duration |");
|
||||
lines.push("|-------|------:|-------:|-------:|--------:|---------:|");
|
||||
|
||||
for (const suite of suites) {
|
||||
const { results } = suite;
|
||||
const icon = getStatusIcon(results);
|
||||
const flaky = results.flaky > 0 ? ` (${results.flaky} flaky)` : "";
|
||||
lines.push(
|
||||
`| ${icon} ${suite.name} | ${results.total} | ${results.passed} | ${results.failed}${flaky} | ${results.skipped} | ${formatDuration(results.duration)} |`,
|
||||
);
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
const suitesWithCoverage = suites.filter((s) => s.coverage);
|
||||
if (suitesWithCoverage.length > 0) {
|
||||
lines.push("### Coverage");
|
||||
lines.push("");
|
||||
lines.push("| Suite | Lines | Branches | Functions | Statements |");
|
||||
lines.push("|-------|------:|---------:|----------:|-----------:|");
|
||||
|
||||
for (const suite of suitesWithCoverage) {
|
||||
const c = suite.coverage;
|
||||
lines.push(
|
||||
`| ${suite.name} | ${c.lines}% | ${c.branches}% | ${c.functions}% | ${c.statements}% |`,
|
||||
);
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
const allFiles = suitesWithCoverage.flatMap(
|
||||
(s) =>
|
||||
s.coverage.files?.map((f) => ({
|
||||
...f,
|
||||
suite: s.name,
|
||||
})) ?? [],
|
||||
);
|
||||
|
||||
if (allFiles.length > 0) {
|
||||
lines.push("<details>");
|
||||
lines.push(`<summary>File coverage (${allFiles.length} files)</summary>`);
|
||||
lines.push("");
|
||||
|
||||
for (const suite of suitesWithCoverage) {
|
||||
if (!suite.coverage.files?.length) {
|
||||
continue;
|
||||
}
|
||||
lines.push(`#### ${suite.name}`);
|
||||
lines.push("");
|
||||
lines.push("| File | Lines | Branches | Functions |");
|
||||
lines.push("|------|------:|---------:|----------:|");
|
||||
for (const file of suite.coverage.files) {
|
||||
lines.push(
|
||||
`| ${file.file} | ${file.lines}% | ${file.branches}% | ${file.functions}% |`,
|
||||
);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
|
||||
lines.push("</details>");
|
||||
lines.push("");
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
if (values["pr-comment"]) {
|
||||
const artifactsDir = values["artifacts-dir"];
|
||||
if (!artifactsDir || !existsSync(artifactsDir)) {
|
||||
console.error(`Artifacts directory not found: ${artifactsDir}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const suites = discoverArtifacts(artifactsDir);
|
||||
const markdown = buildPrComment(suites);
|
||||
process.stdout.write(markdown);
|
||||
} else {
|
||||
const summaryFile = process.env.GITHUB_STEP_SUMMARY;
|
||||
if (!summaryFile) {
|
||||
console.error("GITHUB_STEP_SUMMARY is not set");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const testData = readJson(values.json);
|
||||
if (!testData) {
|
||||
const fallback = `### \u26a0\ufe0f ${values.name}\n\nNo test results found at \`${values.json}\`\n\n`;
|
||||
appendFileSync(summaryFile, fallback);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const results =
|
||||
values.framework === "playwright"
|
||||
? parsePlaywrightResults(testData)
|
||||
: parseVitestResults(testData);
|
||||
|
||||
const coverageData = values.coverage ? readJson(values.coverage) : undefined;
|
||||
const coverage = coverageData
|
||||
? parseCoverageSummary(coverageData)
|
||||
: undefined;
|
||||
|
||||
const markdown = buildMarkdown(values.name, results, coverage);
|
||||
appendFileSync(summaryFile, markdown);
|
||||
}
|
||||
@@ -35,7 +35,12 @@ jobs:
|
||||
close_template:
|
||||
runs-on: ubuntu-latest
|
||||
needs: parse_template
|
||||
if: ${{ needs.parse_template.outputs.uses_template == 'false' && github.event.pull_request.state != 'closed' }}
|
||||
if: >-
|
||||
${{
|
||||
needs.parse_template.outputs.uses_template == 'false'
|
||||
&& github.event.pull_request.state != 'closed'
|
||||
&& !contains(github.event.pull_request.labels.*.name, 'auto-closed:template')
|
||||
}}
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
@@ -66,7 +71,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: gh pr edit "$PR_NUMBER" --add-label "auto-closed:template"
|
||||
run: gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --add-label "auto-closed:template"
|
||||
|
||||
close_llm:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -113,7 +118,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: gh pr edit "$PR_NUMBER" --remove-label "auto-closed:template" || true
|
||||
run: gh pr edit "$PR_NUMBER" --repo "${{ github.repository }}" --remove-label "auto-closed:template" || true
|
||||
|
||||
- name: Check for remaining auto-closed labels
|
||||
id: check_labels
|
||||
@@ -121,7 +126,7 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
REMAINING=$(gh pr view "$PR_NUMBER" --json labels \
|
||||
REMAINING=$(gh pr view "$PR_NUMBER" --repo "${{ github.repository }}" --json labels \
|
||||
--jq '[.labels[].name | select(startswith("auto-closed:"))] | length')
|
||||
echo "remaining=$REMAINING" >> "$GITHUB_OUTPUT"
|
||||
|
||||
|
||||
@@ -51,14 +51,14 @@ jobs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Check what should run
|
||||
id: check
|
||||
uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2
|
||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
||||
with:
|
||||
github-token: ${{ steps.token.outputs.token }}
|
||||
filters: |
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
|
||||
- name: Restore Gradle Cache
|
||||
id: cache-gradle-restore
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
key: build-mobile-gradle-${{ runner.os }}-main
|
||||
|
||||
- name: Setup Flutter SDK
|
||||
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0
|
||||
uses: subosito/flutter-action@0ca7a949e71ae44c8e688a51c5e7e93b2c87e295 # v2.22.0
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version-file: ./mobile/pubspec.yaml
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
|
||||
- name: Save Gradle Cache
|
||||
id: cache-gradle-save
|
||||
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
if: github.ref == 'refs/heads/main'
|
||||
with:
|
||||
path: |
|
||||
@@ -185,13 +185,13 @@ jobs:
|
||||
run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Flutter SDK
|
||||
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2
|
||||
uses: subosito/flutter-action@0ca7a949e71ae44c8e688a51c5e7e93b2c87e295 # v2.22.0
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version-file: ./mobile/pubspec.yaml
|
||||
@@ -210,7 +210,7 @@ jobs:
|
||||
working-directory: ./mobile
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
|
||||
with:
|
||||
ruby-version: '3.3'
|
||||
bundler-cache: true
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
actions: write
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check for breaking API changes
|
||||
uses: oasdiff/oasdiff-action/breaking@748daafaf3aac877a36307f842a48d55db938ac8 # v0.0.31
|
||||
uses: oasdiff/oasdiff-action/breaking@2a37bc82462349c03a533b8b608bebbaf57b3e60 # v0.0.33
|
||||
with:
|
||||
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
||||
revision: open-api/immich-openapi-specs.json
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
working-directory: ./cli
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
needs: [get_body, should_run]
|
||||
if: ${{ needs.should_run.outputs.should_run == 'true' }}
|
||||
container:
|
||||
image: ghcr.io/immich-app/mdq:main@sha256:4f9860d04c88f7f87861f8ee84bfeedaec15ed7ca5ca87bc7db44b036f81645f
|
||||
image: ghcr.io/immich-app/mdq:main@sha256:df7188ba88abb0800d73cc97d3633280f0c0c3d4c441d678225067bf154150fb
|
||||
outputs:
|
||||
checked: ${{ steps.get_checkbox.outputs.checked }}
|
||||
steps:
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
|
||||
uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
|
||||
uses: github/codeql-action/autobuild@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -83,6 +83,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
|
||||
uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
@@ -23,14 +23,14 @@ jobs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Check what should run
|
||||
id: check
|
||||
uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2
|
||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
||||
with:
|
||||
github-token: ${{ steps.token.outputs.token }}
|
||||
filters: |
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
suffixes: '-rocm'
|
||||
platforms: linux/amd64
|
||||
runner-mapping: '{"linux/amd64": "pokedex-large"}'
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@bd49ed7a5a6022149f79b6564df48177476a822b # multi-runner-build-workflow-v2.2.1
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@61a0fc2b41524edcc7c9fffb8bb178e6b0ccf21d # multi-runner-build-workflow-v2.3.0
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
name: Build and Push Server
|
||||
needs: pre-job
|
||||
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@bd49ed7a5a6022149f79b6564df48177476a822b # multi-runner-build-workflow-v2.2.1
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@61a0fc2b41524edcc7c9fffb8bb178e6b0ccf21d # multi-runner-build-workflow-v2.3.0
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
@@ -178,7 +178,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
steps:
|
||||
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
|
||||
- uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
|
||||
with:
|
||||
needs: ${{ toJSON(needs) }}
|
||||
|
||||
@@ -189,6 +189,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
steps:
|
||||
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
|
||||
- uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
|
||||
with:
|
||||
needs: ${{ toJSON(needs) }}
|
||||
|
||||
@@ -21,14 +21,14 @@ jobs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Check what should run
|
||||
id: check
|
||||
uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2
|
||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
||||
with:
|
||||
github-token: ${{ steps.token.outputs.token }}
|
||||
filters: |
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
artifact: ${{ steps.get-artifact.outputs.result }}
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@dab18118da6476e8237ac94080fd937983fecd42 # use-mise-action-v1.1.2
|
||||
uses: immich-app/devtools/actions/use-mise@035e80a7d4355d5f087ffb95db9e4a0944c04e56 # use-mise-action-v1.1.3
|
||||
|
||||
- name: Load parameters
|
||||
id: parameters
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@dab18118da6476e8237ac94080fd937983fecd42 # use-mise-action-v1.1.2
|
||||
uses: immich-app/devtools/actions/use-mise@035e80a7d4355d5f087ffb95db9e4a0944c04e56 # use-mise-action-v1.1.3
|
||||
|
||||
- name: Destroy Docs Subdomain
|
||||
env:
|
||||
|
||||
@@ -14,13 +14,13 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Require PR to have a changelog label
|
||||
uses: mheap/github-action-required-labels@8afbe8ae6ab7647d0c9f0cfa7c2f939650d22509 # v5.5.1
|
||||
uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5.5.2
|
||||
with:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
mode: exactly
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
ref: main
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@6ee6290f1cbc4156c0bdd66691b2c144ef8df19a # v7.4.0
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
@@ -142,7 +142,7 @@ jobs:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
||||
with:
|
||||
draft: true
|
||||
tag_name: ${{ needs.bump_version.outputs.version }}
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
working-directory: ./open-api/typescript-sdk
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
@@ -20,14 +20,14 @@ jobs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Check what should run
|
||||
id: check
|
||||
uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2
|
||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
||||
with:
|
||||
github-token: ${{ steps.token.outputs.token }}
|
||||
filters: |
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
working-directory: ./mobile
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Flutter SDK
|
||||
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0
|
||||
uses: subosito/flutter-action@0ca7a949e71ae44c8e688a51c5e7e93b2c87e295 # v2.22.0
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version-file: ./mobile/pubspec.yaml
|
||||
|
||||
+93
-355
@@ -17,14 +17,14 @@ jobs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Check what should run
|
||||
id: check
|
||||
uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2
|
||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
||||
with:
|
||||
github-token: ${{ steps.token.outputs.token }}
|
||||
filters: |
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
working-directory: ./server
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -94,20 +94,8 @@ jobs:
|
||||
run: pnpm check
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Run small tests & coverage
|
||||
run: pnpm test --reporter=default --reporter=json --outputFile test-results.json --coverage --coverage.reporter=text --coverage.reporter=json-summary --coverage.reporter=json
|
||||
run: pnpm test
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Write test summary
|
||||
if: always()
|
||||
run: node ${{ github.workspace }}/.github/scripts/write-test-summary.mjs --json test-results.json --name "Server Unit Tests" --framework vitest --coverage coverage/coverage-summary.json
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: report-server-unit
|
||||
path: |
|
||||
server/test-results.json
|
||||
server/coverage/coverage-summary.json
|
||||
retention-days: 1
|
||||
cli-unit-tests:
|
||||
name: Unit Test CLI
|
||||
needs: pre-job
|
||||
@@ -120,7 +108,7 @@ jobs:
|
||||
working-directory: ./cli
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -153,18 +141,8 @@ jobs:
|
||||
run: pnpm check
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Run unit tests & coverage
|
||||
run: pnpm test --reporter=default --reporter=json --outputFile test-results.json
|
||||
run: pnpm test
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Write test summary
|
||||
if: always()
|
||||
run: node ${{ github.workspace }}/.github/scripts/write-test-summary.mjs --json test-results.json --name "CLI Unit Tests" --framework vitest
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: report-cli-unit
|
||||
path: cli/test-results.json
|
||||
retention-days: 1
|
||||
cli-unit-tests-win:
|
||||
name: Unit Test CLI (Windows)
|
||||
needs: pre-job
|
||||
@@ -177,7 +155,7 @@ jobs:
|
||||
working-directory: ./cli
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -205,18 +183,8 @@ jobs:
|
||||
run: pnpm check
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Run unit tests & coverage
|
||||
run: pnpm test --reporter=default --reporter=json --outputFile test-results.json
|
||||
run: pnpm test
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Write test summary
|
||||
if: always()
|
||||
run: node ${{ github.workspace }}/.github/scripts/write-test-summary.mjs --json test-results.json --name "CLI Unit Tests (Windows)" --framework vitest
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: report-cli-unit-win
|
||||
path: cli/test-results.json
|
||||
retention-days: 1
|
||||
web-lint:
|
||||
name: Lint Web
|
||||
needs: pre-job
|
||||
@@ -229,7 +197,7 @@ jobs:
|
||||
working-directory: ./web
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -273,7 +241,7 @@ jobs:
|
||||
working-directory: ./web
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -300,20 +268,8 @@ jobs:
|
||||
run: pnpm check:typescript
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Run unit tests & coverage
|
||||
run: pnpm test --reporter=default --reporter=json --outputFile test-results.json --coverage --coverage.reporter=text --coverage.reporter=json-summary --coverage.reporter=json
|
||||
run: pnpm test
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Write test summary
|
||||
if: always()
|
||||
run: node ${{ github.workspace }}/.github/scripts/write-test-summary.mjs --json test-results.json --name "Web Unit Tests" --framework vitest --coverage coverage/coverage-summary.json
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: report-web-unit
|
||||
path: |
|
||||
web/test-results.json
|
||||
web/coverage/coverage-summary.json
|
||||
retention-days: 1
|
||||
i18n-tests:
|
||||
name: Test i18n
|
||||
needs: pre-job
|
||||
@@ -323,7 +279,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -371,7 +327,7 @@ jobs:
|
||||
working-directory: ./e2e
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -417,7 +373,7 @@ jobs:
|
||||
working-directory: ./server
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -439,20 +395,8 @@ jobs:
|
||||
- name: Run pnpm install
|
||||
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
|
||||
- name: Run medium tests
|
||||
run: pnpm test:medium --reporter=default --reporter=json --outputFile test-results-medium.json --coverage --coverage.reporter=text --coverage.reporter=json-summary --coverage.reporter=json
|
||||
run: pnpm test:medium
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Write test summary
|
||||
if: always()
|
||||
run: node ${{ github.workspace }}/.github/scripts/write-test-summary.mjs --json test-results-medium.json --name "Server Medium Tests" --framework vitest --coverage coverage/coverage-summary.json
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: report-server-medium
|
||||
path: |
|
||||
server/test-results-medium.json
|
||||
server/coverage/coverage-summary.json
|
||||
retention-days: 1
|
||||
e2e-tests-server-cli:
|
||||
name: End-to-End Tests (Server & CLI)
|
||||
needs: pre-job
|
||||
@@ -460,7 +404,6 @@ jobs:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./e2e
|
||||
@@ -469,114 +412,63 @@ jobs:
|
||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: 'recursive'
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
- name: Compute server cache key
|
||||
run: |
|
||||
BUILD_ARGS=$'DEVICE=cpu\n'
|
||||
HASH=$(sha256sum <<< "${BUILD_ARGS}" | cut -d' ' -f1)
|
||||
ARCH=$(echo "${{ runner.arch }}" | tr '[:upper:]' '[:lower:]')
|
||||
[[ "$ARCH" == "x64" ]] && ARCH="amd64"
|
||||
echo "SERVER_CACHE_KEY=linux-${ARCH}-${HASH}-main" >> $GITHUB_ENV
|
||||
- name: Build Docker images from cache
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml --profile test build
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version-file: './e2e/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- name: Run setup typescript-sdk
|
||||
run: pnpm install --frozen-lockfile && pnpm build
|
||||
working-directory: ./open-api/typescript-sdk
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Run setup web
|
||||
run: pnpm install --frozen-lockfile && pnpm exec svelte-kit sync
|
||||
working-directory: ./web
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Run setup cli
|
||||
run: pnpm install --frozen-lockfile && pnpm build
|
||||
working-directory: ./cli
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Start Docker Compose
|
||||
run: docker compose up -d --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Run e2e tests (api & cli)
|
||||
run: docker compose --profile test run --rm e2e-runner pnpm test --reporter=default --reporter=json --outputFile playwright-report/test-results.json
|
||||
env:
|
||||
VITEST_DISABLE_DOCKER_SETUP: true
|
||||
run: pnpm test
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Write test summary
|
||||
if: always()
|
||||
run: node ${{ github.workspace }}/.github/scripts/write-test-summary.mjs --json playwright-report/test-results.json --name "E2E Server & CLI Tests (${{ matrix.runner }})" --framework vitest
|
||||
- name: Upload test results
|
||||
- name: Run e2e tests (maintenance)
|
||||
env:
|
||||
VITEST_DISABLE_DOCKER_SETUP: true
|
||||
run: pnpm test:maintenance
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: report-e2e-server-cli-${{ matrix.runner }}
|
||||
path: e2e/playwright-report/test-results.json
|
||||
retention-days: 1
|
||||
- name: Capture Docker logs
|
||||
if: always()
|
||||
run: docker compose logs --no-color > docker-compose-logs.txt
|
||||
working-directory: ./e2e
|
||||
- name: Archive Docker logs
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
if: always()
|
||||
with:
|
||||
name: e2e-server-docker-logs-${{ matrix.runner }}
|
||||
path: e2e/docker-compose-logs.txt
|
||||
e2e-tests-server-maintenance:
|
||||
name: End-to-End Tests (Server Maintenance)
|
||||
needs: pre-job
|
||||
if: ${{ fromJSON(needs.pre-job.outputs.should_run).e2e == true || fromJSON(needs.pre-job.outputs.should_run).server == true || fromJSON(needs.pre-job.outputs.should_run).cli == true }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./e2e
|
||||
strategy:
|
||||
matrix:
|
||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: 'recursive'
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
- name: Compute server cache key
|
||||
run: |
|
||||
BUILD_ARGS=$'DEVICE=cpu\n'
|
||||
HASH=$(sha256sum <<< "${BUILD_ARGS}" | cut -d' ' -f1)
|
||||
ARCH=$(echo "${{ runner.arch }}" | tr '[:upper:]' '[:lower:]')
|
||||
[[ "$ARCH" == "x64" ]] && ARCH="amd64"
|
||||
echo "SERVER_CACHE_KEY=linux-${ARCH}-${HASH}-main" >> $GITHUB_ENV
|
||||
- name: Build Docker images from cache
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml --profile test build
|
||||
- name: Start Docker Compose
|
||||
run: docker compose up -d --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Run e2e tests (maintenance)
|
||||
run: docker compose --profile test run --rm e2e-runner pnpm test:maintenance --reporter=default --reporter=json --outputFile playwright-report/test-results.json
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Write test summary
|
||||
if: always()
|
||||
run: node ${{ github.workspace }}/.github/scripts/write-test-summary.mjs --json playwright-report/test-results.json --name "E2E Server Maintenance Tests (${{ matrix.runner }})" --framework vitest
|
||||
- name: Upload test results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: report-e2e-server-maintenance-${{ matrix.runner }}
|
||||
path: e2e/playwright-report/test-results.json
|
||||
retention-days: 1
|
||||
- name: Capture Docker logs
|
||||
if: always()
|
||||
run: docker compose logs --no-color > docker-compose-logs.txt
|
||||
- name: Archive Docker logs
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
if: always()
|
||||
with:
|
||||
name: e2e-server-maintenance-docker-logs-${{ matrix.runner }}
|
||||
path: e2e/docker-compose-logs.txt
|
||||
e2e-tests-web:
|
||||
name: End-to-End Tests (Web)
|
||||
needs: pre-job
|
||||
@@ -584,7 +476,6 @@ jobs:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./e2e
|
||||
@@ -593,179 +484,65 @@ jobs:
|
||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: 'recursive'
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
- name: Compute server cache key
|
||||
run: |
|
||||
BUILD_ARGS=$'DEVICE=cpu\n'
|
||||
HASH=$(sha256sum <<< "${BUILD_ARGS}" | cut -d' ' -f1)
|
||||
ARCH=$(echo "${{ runner.arch }}" | tr '[:upper:]' '[:lower:]')
|
||||
[[ "$ARCH" == "x64" ]] && ARCH="amd64"
|
||||
echo "SERVER_CACHE_KEY=linux-${ARCH}-${HASH}-main" >> $GITHUB_ENV
|
||||
- name: Build Docker images from cache
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml --profile test build
|
||||
- name: Start Docker Compose
|
||||
run: docker compose up -d --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version-file: './e2e/.nvmrc'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||
- name: Run setup typescript-sdk
|
||||
run: pnpm install --frozen-lockfile && pnpm build
|
||||
working-directory: ./open-api/typescript-sdk
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Install Playwright Browsers
|
||||
run: pnpm exec playwright install chromium --only-shell
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Docker build
|
||||
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Run e2e tests (web)
|
||||
run: docker compose --profile test run --rm e2e-runner pnpm test:web
|
||||
env:
|
||||
PLAYWRIGHT_DISABLE_WEBSERVER: true
|
||||
run: pnpm test:web
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Write test summary (web)
|
||||
if: always()
|
||||
run: node ${{ github.workspace }}/.github/scripts/write-test-summary.mjs --json playwright-report/test-results.json --name "E2E Web Tests (${{ matrix.runner }})" --framework playwright
|
||||
- name: Upload test results (web)
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: report-e2e-web-${{ matrix.runner }}
|
||||
path: e2e/playwright-report/test-results.json
|
||||
retention-days: 1
|
||||
- name: Archive e2e test (web) results
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
if: success() || failure()
|
||||
with:
|
||||
name: e2e-web-test-results-${{ matrix.runner }}
|
||||
path: e2e/playwright-report/
|
||||
- name: Capture Docker logs
|
||||
if: always()
|
||||
run: docker compose logs --no-color > docker-compose-logs.txt
|
||||
- name: Archive Docker logs
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
if: always()
|
||||
with:
|
||||
name: e2e-web-docker-logs-${{ matrix.runner }}
|
||||
path: e2e/docker-compose-logs.txt
|
||||
e2e-tests-web-ui:
|
||||
name: End-to-End Tests (Web UI)
|
||||
needs: pre-job
|
||||
if: ${{ fromJSON(needs.pre-job.outputs.should_run).e2e == true || fromJSON(needs.pre-job.outputs.should_run).web == true }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./e2e
|
||||
strategy:
|
||||
matrix:
|
||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: 'recursive'
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
- name: Compute server cache key
|
||||
run: |
|
||||
BUILD_ARGS=$'DEVICE=cpu\n'
|
||||
HASH=$(sha256sum <<< "${BUILD_ARGS}" | cut -d' ' -f1)
|
||||
ARCH=$(echo "${{ runner.arch }}" | tr '[:upper:]' '[:lower:]')
|
||||
[[ "$ARCH" == "x64" ]] && ARCH="amd64"
|
||||
echo "SERVER_CACHE_KEY=linux-${ARCH}-${HASH}-main" >> $GITHUB_ENV
|
||||
- name: Build Docker images from cache
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml --profile test build
|
||||
- name: Start Docker Compose
|
||||
run: docker compose up -d --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Run ui tests (web)
|
||||
run: docker compose --profile test run --rm e2e-runner pnpm test:web:ui
|
||||
env:
|
||||
PLAYWRIGHT_DISABLE_WEBSERVER: true
|
||||
run: pnpm test:web:ui
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Write test summary (ui)
|
||||
if: always()
|
||||
run: node ${{ github.workspace }}/.github/scripts/write-test-summary.mjs --json playwright-report/test-results.json --name "E2E Web UI Tests (${{ matrix.runner }})" --framework playwright
|
||||
- name: Upload test results (ui)
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: report-e2e-web-ui-${{ matrix.runner }}
|
||||
path: e2e/playwright-report/test-results.json
|
||||
retention-days: 1
|
||||
- name: Archive ui test (web) results
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
if: success() || failure()
|
||||
with:
|
||||
name: e2e-ui-test-results-${{ matrix.runner }}
|
||||
path: e2e/playwright-report/
|
||||
- name: Capture Docker logs
|
||||
if: always()
|
||||
run: docker compose logs --no-color > docker-compose-logs.txt
|
||||
- name: Archive Docker logs
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
if: always()
|
||||
with:
|
||||
name: e2e-web-ui-docker-logs-${{ matrix.runner }}
|
||||
path: e2e/docker-compose-logs.txt
|
||||
e2e-tests-web-maintenance:
|
||||
name: End-to-End Tests (Web Maintenance)
|
||||
needs: pre-job
|
||||
if: ${{ fromJSON(needs.pre-job.outputs.should_run).e2e == true || fromJSON(needs.pre-job.outputs.should_run).web == true }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./e2e
|
||||
strategy:
|
||||
matrix:
|
||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: 'recursive'
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
- name: Compute server cache key
|
||||
run: |
|
||||
BUILD_ARGS=$'DEVICE=cpu\n'
|
||||
HASH=$(sha256sum <<< "${BUILD_ARGS}" | cut -d' ' -f1)
|
||||
ARCH=$(echo "${{ runner.arch }}" | tr '[:upper:]' '[:lower:]')
|
||||
[[ "$ARCH" == "x64" ]] && ARCH="amd64"
|
||||
echo "SERVER_CACHE_KEY=linux-${ARCH}-${HASH}-main" >> $GITHUB_ENV
|
||||
- name: Build Docker images from cache
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml --profile test build
|
||||
- name: Start Docker Compose
|
||||
run: docker compose up -d --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Run maintenance tests
|
||||
run: docker compose --profile test run --rm e2e-runner pnpm test:web:maintenance
|
||||
env:
|
||||
PLAYWRIGHT_DISABLE_WEBSERVER: true
|
||||
run: pnpm test:web:maintenance
|
||||
if: ${{ !cancelled() }}
|
||||
- name: Write test summary (maintenance)
|
||||
if: always()
|
||||
run: node ${{ github.workspace }}/.github/scripts/write-test-summary.mjs --json playwright-report/test-results.json --name "E2E Web Maintenance Tests (${{ matrix.runner }})" --framework playwright
|
||||
- name: Upload test results (maintenance)
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: report-e2e-web-maintenance-${{ matrix.runner }}
|
||||
path: e2e/playwright-report/test-results.json
|
||||
retention-days: 1
|
||||
- name: Archive maintenance tests (web) results
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
if: success() || failure()
|
||||
@@ -775,62 +552,23 @@ jobs:
|
||||
- name: Capture Docker logs
|
||||
if: always()
|
||||
run: docker compose logs --no-color > docker-compose-logs.txt
|
||||
working-directory: ./e2e
|
||||
- name: Archive Docker logs
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
if: always()
|
||||
with:
|
||||
name: e2e-web-maintenance-docker-logs-${{ matrix.runner }}
|
||||
name: e2e-web-docker-logs-${{ matrix.runner }}
|
||||
path: e2e/docker-compose-logs.txt
|
||||
success-check-e2e:
|
||||
name: End-to-End Tests Success
|
||||
needs:
|
||||
[
|
||||
e2e-tests-server-cli,
|
||||
e2e-tests-server-maintenance,
|
||||
e2e-tests-web,
|
||||
e2e-tests-web-ui,
|
||||
e2e-tests-web-maintenance,
|
||||
]
|
||||
needs: [e2e-tests-server-cli, e2e-tests-web]
|
||||
permissions: {}
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
steps:
|
||||
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
|
||||
- uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
|
||||
with:
|
||||
needs: ${{ toJSON(needs) }}
|
||||
test-report:
|
||||
name: PR Test Report
|
||||
if: github.event_name == 'pull_request' && always()
|
||||
needs:
|
||||
- server-unit-tests
|
||||
- web-unit-tests
|
||||
- server-medium-tests
|
||||
- cli-unit-tests
|
||||
- cli-unit-tests-win
|
||||
- e2e-tests-server-cli
|
||||
- e2e-tests-server-maintenance
|
||||
- e2e-tests-web
|
||||
- e2e-tests-web-ui
|
||||
- e2e-tests-web-maintenance
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: 'report-*'
|
||||
path: artifacts
|
||||
- name: Generate unified report
|
||||
run: node .github/scripts/write-test-summary.mjs --pr-comment --artifacts-dir artifacts > pr-comment.md
|
||||
- name: Post PR comment
|
||||
uses: marocchino/sticky-pull-request-comment@70d2764d1a7d5d9560b100cbea0077fc8f633987 # v3.0.2
|
||||
with:
|
||||
header: test-report
|
||||
path: pr-comment.md
|
||||
mobile-unit-tests:
|
||||
name: Unit Test Mobile
|
||||
needs: pre-job
|
||||
@@ -840,7 +578,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -850,7 +588,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
- name: Setup Flutter SDK
|
||||
uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # v2.21.0
|
||||
uses: subosito/flutter-action@0ca7a949e71ae44c8e688a51c5e7e93b2c87e295 # v2.22.0
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version-file: ./mobile/pubspec.yaml
|
||||
@@ -872,7 +610,7 @@ jobs:
|
||||
working-directory: ./machine-learning
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -882,7 +620,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@6ee6290f1cbc4156c0bdd66691b2c144ef8df19a # v7.4.0
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
with:
|
||||
python-version: 3.11
|
||||
- name: Install dependencies
|
||||
@@ -912,7 +650,7 @@ jobs:
|
||||
working-directory: ./.github
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -942,7 +680,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -963,7 +701,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -1025,7 +763,7 @@ jobs:
|
||||
working-directory: ./server
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
@@ -24,14 +24,14 @@ jobs:
|
||||
should_run: ${{ steps.check.outputs.should_run }}
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Check what should run
|
||||
id: check
|
||||
uses: immich-app/devtools/actions/pre-job@eed0f8b8165ffcb951f2ba854b2dd031935e1d73 # pre-job-action-v2.0.2
|
||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
||||
with:
|
||||
github-token: ${{ steps.token.outputs.token }}
|
||||
filters: |
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }}
|
||||
steps:
|
||||
- id: token
|
||||
uses: immich-app/devtools/actions/create-workflow-token@05e16407c0a5492138bb38139c9d9bf067b40886 # create-workflow-token-action-v1.0.1
|
||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@@ -68,6 +68,6 @@ jobs:
|
||||
permissions: {}
|
||||
if: always()
|
||||
steps:
|
||||
- uses: immich-app/devtools/actions/success-check@68f10eb389bb02a3cf9d1156111964c549eb421b # 0.0.4
|
||||
- uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
|
||||
with:
|
||||
needs: ${{ toJSON(needs) }}
|
||||
|
||||
@@ -24,7 +24,7 @@ e2e-update:
|
||||
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||
|
||||
e2e-down:
|
||||
docker compose -f ./e2e/docker-compose.yml --profile test down --remove-orphans
|
||||
docker compose -f ./e2e/docker-compose.yml down --remove-orphans
|
||||
|
||||
prod:
|
||||
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
||||
@@ -101,30 +101,10 @@ check-web:
|
||||
pnpm --filter immich-web run check:svelte
|
||||
test-%:
|
||||
pnpm --filter $(call map-package,$*) run test
|
||||
test-e2e: build-e2e test-e2e-server test-e2e-server-maintenance test-e2e-web test-e2e-web-ui test-e2e-web-maintenance
|
||||
|
||||
test-e2e-server:
|
||||
docker compose -f ./e2e/docker-compose.yml up -d --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||
docker compose -f ./e2e/docker-compose.yml --profile test run --rm e2e-runner pnpm test
|
||||
|
||||
test-e2e-server-maintenance:
|
||||
docker compose -f ./e2e/docker-compose.yml up -d --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||
docker compose -f ./e2e/docker-compose.yml --profile test run --rm e2e-runner pnpm test:maintenance
|
||||
|
||||
test-e2e-web:
|
||||
docker compose -f ./e2e/docker-compose.yml up -d --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||
docker compose -f ./e2e/docker-compose.yml --profile test run --rm e2e-runner pnpm test:web
|
||||
|
||||
test-e2e-web-ui:
|
||||
docker compose -f ./e2e/docker-compose.yml up -d --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||
docker compose -f ./e2e/docker-compose.yml --profile test run --rm e2e-runner pnpm test:web:ui
|
||||
|
||||
test-e2e-web-maintenance:
|
||||
docker compose -f ./e2e/docker-compose.yml up -d --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
|
||||
docker compose -f ./e2e/docker-compose.yml --profile test run --rm e2e-runner pnpm test:web:maintenance
|
||||
|
||||
build-e2e:
|
||||
docker compose -f ./e2e/docker-compose.yml --profile test build
|
||||
test-e2e:
|
||||
docker compose -f ./e2e/docker-compose.yml build
|
||||
pnpm --filter immich-e2e run test
|
||||
pnpm --filter immich-e2e run test:web
|
||||
test-medium:
|
||||
docker run \
|
||||
--rm \
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
24.13.1
|
||||
24.14.0
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.6.2",
|
||||
"version": "2.6.3",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"type": "module",
|
||||
"exports": "./dist/index.js",
|
||||
@@ -20,7 +20,7 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/micromatch": "^4.0.9",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^24.11.0",
|
||||
"@types/node": "^24.12.0",
|
||||
"@vitest/coverage-v8": "^4.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
@@ -68,6 +68,6 @@
|
||||
"micromatch": "^4.0.8"
|
||||
},
|
||||
"volta": {
|
||||
"node": "24.13.1"
|
||||
"node": "24.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ services:
|
||||
IMMICH_THIRD_PARTY_BUG_FEATURE_URL: https://github.com/immich-app/immich/issues
|
||||
IMMICH_THIRD_PARTY_DOCUMENTATION_URL: https://docs.immich.app
|
||||
IMMICH_THIRD_PARTY_SUPPORT_URL: https://docs.immich.app/community-guides
|
||||
IMMICH_HELMET_FILE: 'true'
|
||||
ports:
|
||||
- 9230:9230
|
||||
- 9231:9231
|
||||
|
||||
@@ -97,7 +97,7 @@ services:
|
||||
command: ['./run.sh', '-disable-reporting']
|
||||
ports:
|
||||
- 3000:3000
|
||||
image: grafana/grafana:12.3.2-ubuntu@sha256:6cca4b429a1dc0d37d401dee54825c12d40056c3c6f3f56e3f0d6318ce77749b
|
||||
image: grafana/grafana:12.4.1-ubuntu@sha256:1a20dea76a2778773df17dbc365db86b1a4f2d57772b8590b6311038a3acb1db
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
24.13.1
|
||||
24.14.0
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Duplicates Utility
|
||||
|
||||
Immich comes with a duplicates utility to help you detect assets that look visually similar. The duplicate detection feature relies on machine learning and is enabled by default. For more information about when the duplicate detection job runs, see [Jobs and Workers](/administration/jobs-workers). Once an asset has been processed and added to a duplicate group, it becomes available to review in the "Review duplicates" utility, which can be found [here](https://my.immich.app/utilities/duplicates).
|
||||
|
||||
## Reviewing duplicates
|
||||
|
||||
The review duplicates page allows the user to individually select which assets should be kept and which ones should be trashed. When more than one asset is kept, there is an option to automatically put the kept assets into a stack.
|
||||
|
||||
### Automatic preselection
|
||||
|
||||
When using "Deduplicate All" or viewing suggestions, Immich automatically preselects which assets to keep based on:
|
||||
|
||||
1. **Image size in bytes** — larger files are preferred as they typically have higher quality.
|
||||
2. **Count of EXIF data** — assets with more metadata are preferred.
|
||||
|
||||
### Synchronizing metadata
|
||||
|
||||
When resolving duplicates, metadata from trashed assets is automatically synchronized to the kept assets. The following metadata is synchronized:
|
||||
|
||||
| Name | Description |
|
||||
| ----------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Album | The kept assets will be added to _every_ album that the other assets in the group belong to. |
|
||||
| Favorite | If any of the assets in the group have been added to favorites, every kept asset will also be added to favorites. |
|
||||
| Rating | If one or more assets in the duplicate group have a rating, the highest rating is selected and synchronized to the kept assets. |
|
||||
| Description | Descriptions from each asset are combined together and synchronized to all the kept assets. |
|
||||
| Visibility | The most restrictive visibility is applied to the kept assets. |
|
||||
| Location | Latitude and longitude are copied if all assets with geolocation data in the group share the same coordinates. |
|
||||
| Tag | Tags from all assets in the group are merged and applied to every kept asset. |
|
||||
@@ -3,8 +3,8 @@
|
||||
You may decide that you'd like to modify the style document which is used to
|
||||
draw the maps in Immich. In addition to visual customization, this also allows
|
||||
you to pick your own map tile provider instead of the default one. The default
|
||||
`style.json` for [light theme](https://github.com/immich-app/immich/tree/main/server/resources/style-light.json)
|
||||
and [dark theme](https://github.com/immich-app/immich/blob/main/server/resources/style-dark.json)
|
||||
`style.json` for [light theme](https://tiles.immich.cloud/v1/style/light.json)
|
||||
and [dark theme](https://tiles.immich.cloud/v1/style/dark.json)
|
||||
can be used as a basis for creating your own style.
|
||||
|
||||
There are several sources for already-made `style.json` map themes, as well as
|
||||
|
||||
@@ -29,22 +29,23 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
||||
|
||||
## General
|
||||
|
||||
| Variable | Description | Default | Containers | Workers |
|
||||
| :---------------------------------- | :---------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
|
||||
| `TZ` | Timezone | <sup>\*1</sup> | server | microservices |
|
||||
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
|
||||
| `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
|
||||
| `IMMICH_LOG_FORMAT` | Log output format (`console`, `json`) | `console` | server | api, microservices |
|
||||
| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ | `/data` | server | api, microservices |
|
||||
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
|
||||
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
|
||||
| `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | |
|
||||
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
|
||||
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
|
||||
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
|
||||
| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api |
|
||||
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/administration/system-integrity) | | server | api, microservices |
|
||||
| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` endpoint | `true` | server | api |
|
||||
| Variable | Description | Default | Containers | Workers |
|
||||
| :---------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
|
||||
| `TZ` | Timezone | <sup>\*1</sup> | server | microservices |
|
||||
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
|
||||
| `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
|
||||
| `IMMICH_LOG_FORMAT` | Log output format (`console`, `json`) | `console` | server | api, microservices |
|
||||
| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ | `/data` | server | api, microservices |
|
||||
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
|
||||
| `IMMICH_HELMET_FILE` | Path to a json file with [helmet](https://www.npmjs.com/package/helmet) options. Set to `false` to disable. Set to `true` to use `server/helmet.json`. | `false` | server | api, microservices |
|
||||
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
|
||||
| `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | |
|
||||
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
|
||||
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
|
||||
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
|
||||
| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api |
|
||||
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/administration/system-integrity) | | server | api, microservices |
|
||||
| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` endpoint | `true` | server | api |
|
||||
|
||||
\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
|
||||
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.
|
||||
|
||||
+1
-1
@@ -58,6 +58,6 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"volta": {
|
||||
"node": "24.13.1"
|
||||
"node": "24.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+2
-2
@@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"label": "v2.6.2",
|
||||
"url": "https://docs.v2.6.2.archive.immich.app"
|
||||
"label": "v2.6.3",
|
||||
"url": "https://docs.v2.6.3.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v2.5.6",
|
||||
|
||||
@@ -71,7 +71,6 @@ const setup = async () => {
|
||||
const redirectUris = [
|
||||
'http://127.0.0.1:2285/auth/login',
|
||||
'https://photos.immich.app/oauth/mobile-redirect',
|
||||
...(process.env.EXTRA_REDIRECT_URIS?.split(',').filter(Boolean) ?? []),
|
||||
];
|
||||
const port = 2286;
|
||||
const host = '0.0.0.0';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"name": "@immich/e2e-auth-server",
|
||||
"version": "0.1.0",
|
||||
"packageManager": "pnpm@10.30.3+sha512.c961d1e0a2d8e354ecaa5166b822516668b7f44cb5bd95122d590dd81922f606f5473b6d23ec4a5be05e7fcd18e8488d47d978bbe981872f1145d06e9a740017",
|
||||
"type": "module",
|
||||
"main": "auth-server.ts",
|
||||
"scripts": {
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
24.13.1
|
||||
24.14.0
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
FROM node:22-bookworm-slim
|
||||
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
RUN corepack enable && corepack prepare pnpm@10.30.3 --activate
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
docker.io \
|
||||
unzip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml .pnpmfile.cjs ./
|
||||
COPY open-api/typescript-sdk/package.json open-api/typescript-sdk/
|
||||
COPY cli/package.json cli/
|
||||
COPY web/package.json web/
|
||||
COPY e2e/package.json e2e/
|
||||
COPY e2e-auth-server/package.json e2e-auth-server/
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
COPY open-api/typescript-sdk/ open-api/typescript-sdk/
|
||||
RUN pnpm --filter @immich/sdk build
|
||||
|
||||
COPY cli/ cli/
|
||||
RUN pnpm --filter @immich/cli build && ln -s /app/cli/bin/immich /app/cli/node_modules/.bin/immich
|
||||
|
||||
COPY web/svelte.config.js web/vite.config.ts web/tsconfig.json web/
|
||||
COPY web/src/ web/src/
|
||||
COPY web/static/ web/static/
|
||||
RUN pnpm --filter immich-web exec svelte-kit sync
|
||||
|
||||
COPY e2e/ e2e/
|
||||
COPY e2e-auth-server/ e2e-auth-server/
|
||||
|
||||
RUN pnpm --filter immich-e2e exec playwright install --with-deps chromium
|
||||
|
||||
WORKDIR /app/e2e
|
||||
@@ -1,23 +0,0 @@
|
||||
.vscode/
|
||||
.github/
|
||||
.git/
|
||||
*.log
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
**/node_modules/
|
||||
**/.pnpm-store/
|
||||
**/dist/
|
||||
**/coverage/
|
||||
**/build/
|
||||
|
||||
design/
|
||||
docker/
|
||||
docs/
|
||||
fastlane/
|
||||
machine-learning/
|
||||
misc/
|
||||
mobile/
|
||||
server/
|
||||
plugins/
|
||||
i18n/
|
||||
@@ -1,16 +0,0 @@
|
||||
name: immich-e2e
|
||||
|
||||
services:
|
||||
e2e-auth-server:
|
||||
build:
|
||||
cache_from:
|
||||
- type=gha,scope=e2e-auth-${RUNNER_ARCH:-X64}
|
||||
cache_to:
|
||||
- type=gha,mode=max,scope=e2e-auth-${RUNNER_ARCH:-X64}
|
||||
|
||||
e2e-runner:
|
||||
build:
|
||||
cache_from:
|
||||
- type=gha,scope=e2e-runner-${RUNNER_ARCH:-X64}
|
||||
cache_to:
|
||||
- type=gha,mode=max,scope=e2e-runner-${RUNNER_ARCH:-X64}
|
||||
+2
-30
@@ -5,8 +5,6 @@ services:
|
||||
container_name: immich-e2e-auth-server
|
||||
build:
|
||||
context: ../e2e-auth-server
|
||||
environment:
|
||||
EXTRA_REDIRECT_URIS: http://immich-server:2285/auth/login
|
||||
ports:
|
||||
- 2286:2286
|
||||
|
||||
@@ -17,7 +15,8 @@ services:
|
||||
context: ../
|
||||
dockerfile: server/Dockerfile
|
||||
cache_from:
|
||||
- type=registry,ref=ghcr.io/immich-app/immich-server-build-cache:${SERVER_CACHE_KEY:-linux-amd64-cc099f297acd18c924b35ece3245215b53d106eb2518e3af6415931d055746cd-main}
|
||||
- type=registry,ref=ghcr.io/immich-app/immich-server-build-cache:linux-amd64-cc099f297acd18c924b35ece3245215b53d106eb2518e3af6415931d055746cd-main
|
||||
- type=registry,ref=ghcr.io/immich-app/immich-server-build-cache:linux-arm64-cc099f297acd18c924b35ece3245215b53d106eb2518e3af6415931d055746cd-main
|
||||
args:
|
||||
- BUILD_ID=1234567890
|
||||
- BUILD_IMAGE=e2e
|
||||
@@ -66,30 +65,3 @@ services:
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
start_period: 10s
|
||||
|
||||
e2e-runner:
|
||||
container_name: immich-e2e-runner
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: e2e/Dockerfile.playwright
|
||||
network_mode: 'service:immich-server'
|
||||
shm_size: 1gb
|
||||
environment:
|
||||
PLAYWRIGHT_DB_HOST: database
|
||||
PLAYWRIGHT_DB_PORT: '5432'
|
||||
PLAYWRIGHT_DISABLE_WEBSERVER: 'true'
|
||||
PLAYWRIGHT_AUTH_SERVER_URL: http://e2e-auth-server:2286
|
||||
VITEST_DISABLE_DOCKER_SETUP: 'true'
|
||||
CI: '${CI:-}'
|
||||
volumes:
|
||||
- ./test-assets:/app/e2e/test-assets
|
||||
- ./playwright-report:/app/e2e/playwright-report
|
||||
- ./blob-report:/app/e2e/blob-report
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
depends_on:
|
||||
immich-server:
|
||||
condition: service_started
|
||||
database:
|
||||
condition: service_healthy
|
||||
profiles:
|
||||
- test
|
||||
|
||||
+4
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "2.6.2",
|
||||
"version": "2.6.3",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
@@ -32,7 +32,7 @@
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@socket.io/component-emitter": "^3.1.2",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^24.11.0",
|
||||
"@types/node": "^24.12.0",
|
||||
"@types/pg": "^8.15.1",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
"@types/supertest": "^6.0.2",
|
||||
@@ -50,6 +50,7 @@
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"sharp": "^0.34.5",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"structured-headers": "^2.0.2",
|
||||
"supertest": "^7.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
"typescript-eslint": "^8.28.0",
|
||||
@@ -58,6 +59,6 @@
|
||||
"vitest": "^4.0.0"
|
||||
},
|
||||
"volta": {
|
||||
"node": "24.13.1"
|
||||
"node": "24.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ dotenv.config({ quiet: true, path: resolve(import.meta.dirname, '.env') });
|
||||
|
||||
export const playwrightHost = process.env.PLAYWRIGHT_HOST ?? '127.0.0.1';
|
||||
export const playwrightDbHost = process.env.PLAYWRIGHT_DB_HOST ?? '127.0.0.1';
|
||||
export const playwrightDbPort = process.env.PLAYWRIGHT_DB_PORT ?? '5435';
|
||||
export const playwriteBaseUrl = process.env.PLAYWRIGHT_BASE_URL ?? `http://${playwrightHost}:2285`;
|
||||
export const playwriteSlowMo = Number.parseInt(process.env.PLAYWRIGHT_SLOW_MO ?? '0');
|
||||
export const playwrightDisableWebserver = process.env.PLAYWRIGHT_DISABLE_WEBSERVER;
|
||||
@@ -20,11 +19,7 @@ const config: PlaywrightTestConfig = {
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 4 : 0,
|
||||
reporter: [
|
||||
['html'],
|
||||
['json', { outputFile: 'playwright-report/test-results.json' }],
|
||||
...(process.env.CI ? [['blob', { outputDir: 'blob-report' }] as const] : []),
|
||||
],
|
||||
reporter: 'html',
|
||||
use: {
|
||||
baseURL: playwriteBaseUrl,
|
||||
trace: 'on-first-retry',
|
||||
@@ -48,7 +43,7 @@ const config: PlaywrightTestConfig = {
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
testDir: './src/ui/specs',
|
||||
fullyParallel: true,
|
||||
workers: process.env.CI ? 3 : Math.min(10, Math.max(1, Math.round(cpus().length * 0.75) - 1)),
|
||||
workers: process.env.CI ? 3 : Math.max(1, Math.round(cpus().length * 0.75) - 1),
|
||||
},
|
||||
{
|
||||
name: 'maintenance',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,651 @@
|
||||
import { LoginResponseDto } from '@immich/sdk';
|
||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { app, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
describe('/duplicates', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let user1: LoginResponseDto;
|
||||
let user2: LoginResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
|
||||
admin = await utils.adminSetup();
|
||||
|
||||
[user1, user2] = await Promise.all([
|
||||
utils.userSetup(admin.accessToken, createUserDto.user1),
|
||||
utils.userSetup(admin.accessToken, createUserDto.user2),
|
||||
]);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Reset assets, albums, tags, and stacks between tests to ensure clean state for repeated test runs
|
||||
// Note: We don't reset users since they're set up once in beforeAll
|
||||
// Stack must be reset before asset due to foreign key constraint
|
||||
await utils.resetDatabase(['stack', 'asset', 'album', 'tag']);
|
||||
});
|
||||
|
||||
describe('GET /duplicates', () => {
|
||||
it('should return empty array when no duplicates', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/duplicates')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return duplicate groups with suggestedKeepAssetIds', async () => {
|
||||
// Create assets with different file sizes for duplicate detection
|
||||
const [asset1, asset2] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
// Manually set duplicateId on both assets to create a duplicate group
|
||||
const duplicateId = '00000000-0000-4000-8000-000000000001';
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.get('/duplicates')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([
|
||||
{
|
||||
duplicateId,
|
||||
assets: expect.arrayContaining([
|
||||
expect.objectContaining({ id: asset1.id }),
|
||||
expect.objectContaining({ id: asset2.id }),
|
||||
]),
|
||||
suggestedKeepAssetIds: expect.any(Array),
|
||||
},
|
||||
]);
|
||||
expect(body[0].suggestedKeepAssetIds.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /duplicates/resolve', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/duplicates/resolve')
|
||||
.send({
|
||||
groups: [{ duplicateId: uuidDto.dummy, keepAssetIds: [], trashAssetIds: [] }],
|
||||
});
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should return failure for non-existent duplicate group', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/duplicates/resolve')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({
|
||||
groups: [{ duplicateId: uuidDto.dummy, keepAssetIds: [], trashAssetIds: [] }],
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
status: 'COMPLETED',
|
||||
results: [
|
||||
{
|
||||
duplicateId: uuidDto.dummy,
|
||||
status: 'FAILED',
|
||||
reason: expect.stringContaining('not found or access denied'),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve duplicate group with keepers', async () => {
|
||||
const [asset1, asset2] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
const duplicateId = '00000000-0000-4000-8000-000000000002';
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post('/duplicates/resolve')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({
|
||||
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
status: 'COMPLETED',
|
||||
results: [
|
||||
{
|
||||
duplicateId,
|
||||
status: 'SUCCESS',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Verify side effects: duplicateId cleared on kept asset
|
||||
const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id);
|
||||
expect(keptAsset.duplicateId).toBeNull();
|
||||
|
||||
// Verify side effects: trashed asset is trashed and duplicateId cleared
|
||||
const trashedAsset = await utils.getAssetInfo(user1.accessToken, asset2.id);
|
||||
expect(trashedAsset.isTrashed).toBe(true);
|
||||
expect(trashedAsset.duplicateId).toBeNull();
|
||||
});
|
||||
|
||||
it('should reject when keepAssetIds and trashAssetIds overlap', async () => {
|
||||
const [asset1, asset2] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
const duplicateId = '00000000-0000-4000-8000-000000000003';
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post('/duplicates/resolve')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({
|
||||
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset1.id] }],
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.results[0].status).toBe('FAILED');
|
||||
expect(body.results[0].reason).toContain('disjoint');
|
||||
});
|
||||
|
||||
it('should require keepAssetIds when partially trashing', async () => {
|
||||
const [asset1, asset2] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
const duplicateId = '00000000-0000-4000-8000-000000000004';
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post('/duplicates/resolve')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({
|
||||
groups: [{ duplicateId, keepAssetIds: [], trashAssetIds: [asset1.id] }],
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.results[0].status).toBe('FAILED');
|
||||
expect(body.results[0].reason).toContain('must cover all assets');
|
||||
});
|
||||
|
||||
it('should reject partial resolution (not all assets covered)', async () => {
|
||||
const [asset1, asset2, asset3] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
const duplicateId = '00000000-0000-4000-8000-000000000010';
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset3.id, duplicateId);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post('/duplicates/resolve')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({
|
||||
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.results[0].status).toBe('FAILED');
|
||||
expect(body.results[0].reason).toContain('must cover all assets');
|
||||
});
|
||||
|
||||
it('should reject asset not in duplicate group', async () => {
|
||||
const [asset1, asset2, outsideAsset] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
const duplicateId = '00000000-0000-4000-8000-000000000011';
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post('/duplicates/resolve')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({
|
||||
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [outsideAsset.id] }],
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.results[0].status).toBe('FAILED');
|
||||
expect(body.results[0].reason).toContain('not a member of duplicate group');
|
||||
});
|
||||
|
||||
it('should allow trash-all without keepers', async () => {
|
||||
const [asset1, asset2] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
const duplicateId = '00000000-0000-4000-8000-000000000012';
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post('/duplicates/resolve')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({
|
||||
groups: [{ duplicateId, keepAssetIds: [], trashAssetIds: [asset1.id, asset2.id] }],
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
status: 'COMPLETED',
|
||||
results: [
|
||||
{
|
||||
duplicateId,
|
||||
status: 'SUCCESS',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Verify both assets are trashed
|
||||
const [asset1Info, asset2Info] = await Promise.all([
|
||||
utils.getAssetInfo(user1.accessToken, asset1.id),
|
||||
utils.getAssetInfo(user1.accessToken, asset2.id),
|
||||
]);
|
||||
|
||||
expect(asset1Info.isTrashed).toBe(true);
|
||||
expect(asset1Info.duplicateId).toBeNull();
|
||||
expect(asset2Info.isTrashed).toBe(true);
|
||||
expect(asset2Info.duplicateId).toBeNull();
|
||||
});
|
||||
|
||||
it('should reject cross-user duplicate group access', async () => {
|
||||
const asset1 = await utils.createAsset(user1.accessToken);
|
||||
const asset2 = await utils.createAsset(user2.accessToken);
|
||||
|
||||
const duplicateId = '00000000-0000-4000-8000-000000000013';
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
|
||||
await utils.setAssetDuplicateId(user2.accessToken, asset2.id, duplicateId);
|
||||
|
||||
// User1 tries to resolve a group containing user2's asset
|
||||
const { status, body } = await request(app)
|
||||
.post('/duplicates/resolve')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({
|
||||
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.results[0].status).toBe('FAILED');
|
||||
expect(body.results[0].reason).toContain('not a member of duplicate group');
|
||||
});
|
||||
|
||||
it('should synchronize favorites when enabled', async () => {
|
||||
const [asset1, asset2] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
// Mark one asset as favorite
|
||||
await request(app)
|
||||
.put('/assets')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ ids: [asset2.id], isFavorite: true });
|
||||
|
||||
const duplicateId = '00000000-0000-4000-8000-000000000020';
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post('/duplicates/resolve')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({
|
||||
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.results[0].status).toBe('SUCCESS');
|
||||
|
||||
// Verify favorite was synchronized to keeper
|
||||
const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id);
|
||||
expect(keptAsset.isFavorite).toBe(true);
|
||||
expect(keptAsset.duplicateId).toBeNull();
|
||||
});
|
||||
|
||||
it('should synchronize visibility when enabled', async () => {
|
||||
const [asset1, asset2] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
// Archive one asset
|
||||
await utils.archiveAssets(user1.accessToken, [asset2.id]);
|
||||
|
||||
const duplicateId = '00000000-0000-4000-8000-000000000021';
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post('/duplicates/resolve')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({
|
||||
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.results[0].status).toBe('SUCCESS');
|
||||
|
||||
// Verify visibility was synchronized to keeper
|
||||
const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id);
|
||||
expect(keptAsset.visibility).toBe('archive');
|
||||
expect(keptAsset.duplicateId).toBeNull();
|
||||
});
|
||||
|
||||
it('should synchronize rating when enabled', async () => {
|
||||
const [asset1, asset2] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
// Set rating on one asset
|
||||
await request(app)
|
||||
.put('/assets')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ ids: [asset2.id], rating: 5 });
|
||||
|
||||
const duplicateId = '00000000-0000-4000-8000-000000000022';
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post('/duplicates/resolve')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({
|
||||
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.results[0].status).toBe('SUCCESS');
|
||||
|
||||
// Verify rating was synchronized to keeper
|
||||
const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id);
|
||||
expect(keptAsset.exifInfo?.rating).toBe(5);
|
||||
expect(keptAsset.duplicateId).toBeNull();
|
||||
});
|
||||
|
||||
it('should synchronize description when enabled', async () => {
|
||||
const [asset1, asset2] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
// Set description on one asset
|
||||
await request(app)
|
||||
.put('/assets')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ ids: [asset2.id], description: 'Test description for duplicate' });
|
||||
|
||||
const duplicateId = '00000000-0000-4000-8000-000000000023';
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post('/duplicates/resolve')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({
|
||||
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.results[0].status).toBe('SUCCESS');
|
||||
|
||||
// Verify description was synchronized to keeper
|
||||
const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id);
|
||||
expect(keptAsset.exifInfo?.description).toBe('Test description for duplicate');
|
||||
expect(keptAsset.duplicateId).toBeNull();
|
||||
});
|
||||
|
||||
it('should synchronize location when enabled', async () => {
|
||||
const [asset1, asset2] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
// Set location on one asset
|
||||
await request(app)
|
||||
.put('/assets')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ ids: [asset2.id], latitude: 40.7128, longitude: -74.006 });
|
||||
|
||||
const duplicateId = '00000000-0000-4000-8000-000000000024';
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post('/duplicates/resolve')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({
|
||||
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.results[0].status).toBe('SUCCESS');
|
||||
|
||||
// Verify location was synchronized to keeper
|
||||
const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id);
|
||||
expect(keptAsset.exifInfo?.latitude).toBe(40.7128);
|
||||
expect(keptAsset.exifInfo?.longitude).toBe(-74.006);
|
||||
expect(keptAsset.duplicateId).toBeNull();
|
||||
});
|
||||
|
||||
it('should synchronize albums when enabled', async () => {
|
||||
const [asset1, asset2] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
// Create albums and add assets to different albums
|
||||
const album1 = await utils.createAlbum(user1.accessToken, {
|
||||
albumName: 'Album 1',
|
||||
assetIds: [asset1.id],
|
||||
});
|
||||
const album2 = await utils.createAlbum(user1.accessToken, {
|
||||
albumName: 'Album 2',
|
||||
assetIds: [asset2.id],
|
||||
});
|
||||
|
||||
const duplicateId = '00000000-0000-4000-8000-000000000025';
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post('/duplicates/resolve')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({
|
||||
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.results[0].status).toBe('SUCCESS');
|
||||
|
||||
// Verify keeper is now in both albums
|
||||
const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id);
|
||||
expect(keptAsset.duplicateId).toBeNull();
|
||||
|
||||
// Check albums directly
|
||||
const { status: album1Status, body: album1Body } = await request(app)
|
||||
.get(`/albums/${album1.id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
const { status: album2Status, body: album2Body } = await request(app)
|
||||
.get(`/albums/${album2.id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
expect(album1Status).toBe(200);
|
||||
expect(album2Status).toBe(200);
|
||||
expect(album1Body.assets.map((a: any) => a.id)).toContain(asset1.id);
|
||||
expect(album2Body.assets.map((a: any) => a.id)).toContain(asset1.id);
|
||||
});
|
||||
|
||||
it('should synchronize tags when enabled', async () => {
|
||||
const [asset1, asset2] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
// Wait for metadata extraction to complete before adding tags
|
||||
// Otherwise, metadata jobs will race and overwrite our tags
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
|
||||
// Create tags and tag assets differently
|
||||
const tags = await utils.upsertTags(user1.accessToken, ['tag1', 'tag2']);
|
||||
await utils.tagAssets(user1.accessToken, tags[0].id, [asset1.id]);
|
||||
await utils.tagAssets(user1.accessToken, tags[1].id, [asset2.id]);
|
||||
|
||||
const duplicateId = '00000000-0000-4000-8000-000000000026';
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post('/duplicates/resolve')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({
|
||||
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.results[0].status).toBe('SUCCESS');
|
||||
|
||||
// Verify keeper has both tags
|
||||
const keptAsset = await utils.getAssetInfo(user1.accessToken, asset1.id);
|
||||
expect(keptAsset.duplicateId).toBeNull();
|
||||
expect(keptAsset.tags).toBeDefined();
|
||||
const tagIds = keptAsset.tags?.map((t) => t.id) || [];
|
||||
expect(tagIds).toContain(tags[0].id);
|
||||
expect(tagIds).toContain(tags[1].id);
|
||||
});
|
||||
|
||||
it('should handle batch resolve with mixed success and failure', async () => {
|
||||
// Create first group that will succeed
|
||||
const [asset1, asset2] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
const duplicateId1 = '00000000-0000-4000-8000-000000000027';
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId1);
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId1);
|
||||
|
||||
// Create second group with non-existent duplicate ID (will fail)
|
||||
const fakeId = '00000000-0000-4000-8000-000000000099';
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post('/duplicates/resolve')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({
|
||||
groups: [
|
||||
{ duplicateId: duplicateId1, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] },
|
||||
{ duplicateId: fakeId, keepAssetIds: [], trashAssetIds: [] },
|
||||
],
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.status).toBe('COMPLETED');
|
||||
expect(body.results).toHaveLength(2);
|
||||
|
||||
// First group should succeed
|
||||
expect(body.results[0].duplicateId).toBe(duplicateId1);
|
||||
expect(body.results[0].status).toBe('SUCCESS');
|
||||
|
||||
// Second group should fail
|
||||
expect(body.results[1].duplicateId).toBe(fakeId);
|
||||
expect(body.results[1].status).toBe('FAILED');
|
||||
expect(body.results[1].reason).toContain('not found or access denied');
|
||||
|
||||
// Verify first group was actually resolved despite second failure
|
||||
const asset1Info = await utils.getAssetInfo(user1.accessToken, asset1.id);
|
||||
expect(asset1Info.duplicateId).toBeNull();
|
||||
const asset2Info = await utils.getAssetInfo(user1.accessToken, asset2.id);
|
||||
expect(asset2Info.isTrashed).toBe(true);
|
||||
});
|
||||
|
||||
it('should trash assets when trash is enabled', async () => {
|
||||
const [asset1, asset2] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
const duplicateId = '00000000-0000-4000-8000-000000000028';
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
|
||||
|
||||
// Ensure trash is enabled (default)
|
||||
const config = await utils.getSystemConfig(admin.accessToken);
|
||||
expect(config.trash.enabled).toBe(true);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post('/duplicates/resolve')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({
|
||||
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.results[0].status).toBe('SUCCESS');
|
||||
|
||||
// Verify asset is trashed (not deleted)
|
||||
const trashedAsset = await utils.getAssetInfo(user1.accessToken, asset2.id);
|
||||
expect(trashedAsset.isTrashed).toBe(true);
|
||||
});
|
||||
|
||||
it('should delete assets when trash is disabled', async () => {
|
||||
const [asset1, asset2] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
const duplicateId = '00000000-0000-4000-8000-000000000029';
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset1.id, duplicateId);
|
||||
await utils.setAssetDuplicateId(user1.accessToken, asset2.id, duplicateId);
|
||||
|
||||
// Disable trash
|
||||
await request(app)
|
||||
.put('/system-config')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({
|
||||
trash: { enabled: false, days: 30 },
|
||||
});
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post('/duplicates/resolve')
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({
|
||||
groups: [{ duplicateId, keepAssetIds: [asset1.id], trashAssetIds: [asset2.id] }],
|
||||
});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body.results[0].status).toBe('SUCCESS');
|
||||
|
||||
// Asset should be marked as deleted (force delete)
|
||||
const { status: getStatus } = await request(app)
|
||||
.get(`/assets/${asset2.id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
// Asset should still be accessible (soft deleted) but marked as deleted
|
||||
expect(getStatus).toBe(200);
|
||||
|
||||
// Re-enable trash for other tests
|
||||
await utils.resetAdminConfig(admin.accessToken);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,8 @@ export const uuidDto = {
|
||||
invalid: 'invalid-uuid',
|
||||
// valid uuid v4
|
||||
notFound: '00000000-0000-4000-a000-000000000000',
|
||||
dummy: '00000000-0000-4000-a000-000000000001',
|
||||
dummy2: '00000000-0000-4000-a000-000000000002',
|
||||
};
|
||||
|
||||
const adminLoginDto = {
|
||||
|
||||
@@ -10,7 +10,9 @@ describe('/admin/database-backups', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
admin = await utils.adminSetup();
|
||||
admin = await utils.adminSetup({
|
||||
onboarding: false,
|
||||
});
|
||||
await utils.resetBackups(admin.accessToken);
|
||||
});
|
||||
|
||||
@@ -94,7 +96,9 @@ describe('/admin/database-backups', () => {
|
||||
({ status, body }) => status === 200 && !body.maintenanceMode,
|
||||
);
|
||||
|
||||
admin = await utils.adminSetup();
|
||||
admin = await utils.adminSetup({
|
||||
onboarding: false,
|
||||
});
|
||||
});
|
||||
|
||||
it.sequential('should not work when the server is configured', async () => {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const authServer = {
|
||||
internal: 'http://e2e-auth-server:2286',
|
||||
external: process.env.PLAYWRIGHT_AUTH_SERVER_URL ?? 'http://127.0.0.1:2286',
|
||||
external: 'http://127.0.0.1:2286',
|
||||
};
|
||||
|
||||
const mobileOverrideRedirectUri = 'https://photos.immich.app/oauth/mobile-redirect';
|
||||
|
||||
@@ -106,7 +106,7 @@ describe('/shared-links', () => {
|
||||
const resp = await request(shareUrl).get(`/${linkWithAssets.key}`);
|
||||
expect(resp.status).toBe(200);
|
||||
expect(resp.header['content-type']).toContain('text/html');
|
||||
expect(resp.text).toContain(`<meta property="og:image" content="${baseUrl}`);
|
||||
expect(resp.text).toContain(`<meta property="og:image" content="http://127.0.0.1:2285`);
|
||||
});
|
||||
|
||||
it('should fall back to my.immich.app og:image meta tag for shared asset if Host header is not present', async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Permission } from '@immich/sdk';
|
||||
import { stat } from 'node:fs/promises';
|
||||
import { app, baseUrl, immichCli, utils } from 'src/utils';
|
||||
import { app, immichCli, utils } from 'src/utils';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
describe(`immich login`, () => {
|
||||
@@ -33,7 +33,7 @@ describe(`immich login`, () => {
|
||||
const key = await utils.createApiKey(admin.accessToken, [Permission.All]);
|
||||
const { stdout, stderr, exitCode } = await immichCli(['login', app, `${key.secret}`]);
|
||||
expect(stdout.split('\n')).toEqual([
|
||||
`Logging in to ${baseUrl}/api`,
|
||||
'Logging in to http://127.0.0.1:2285/api',
|
||||
'Logged in as admin@immich.cloud',
|
||||
'Wrote auth info to /tmp/immich/auth.yml',
|
||||
]);
|
||||
@@ -50,8 +50,8 @@ describe(`immich login`, () => {
|
||||
const key = await utils.createApiKey(admin.accessToken, [Permission.All]);
|
||||
const { stdout, stderr, exitCode } = await immichCli(['login', app.replaceAll('/api', ''), `${key.secret}`]);
|
||||
expect(stdout.split('\n')).toEqual([
|
||||
`Logging in to ${baseUrl}`,
|
||||
`Discovered API at ${baseUrl}/api`,
|
||||
'Logging in to http://127.0.0.1:2285',
|
||||
'Discovered API at http://127.0.0.1:2285/api',
|
||||
'Logged in as admin@immich.cloud',
|
||||
'Wrote auth info to /tmp/immich/auth.yml',
|
||||
]);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { baseUrl, immichCli, utils } from 'src/utils';
|
||||
import { immichCli, utils } from 'src/utils';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
describe(`immich server-info`, () => {
|
||||
@@ -12,7 +12,7 @@ describe(`immich server-info`, () => {
|
||||
const { stderr, stdout, exitCode } = await immichCli(['server-info']);
|
||||
expect(stdout.split('\n')).toEqual([
|
||||
expect.stringContaining('Server Info (via admin@immich.cloud'),
|
||||
` Url: ${baseUrl}/api`,
|
||||
' Url: http://127.0.0.1:2285/api',
|
||||
expect.stringContaining('Version:'),
|
||||
' Formats:',
|
||||
expect.stringContaining('Images:'),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { LoginResponseDto } from '@immich/sdk';
|
||||
import { test } from '@playwright/test';
|
||||
import { utils } from 'src/utils';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { testAssetDir, utils } from 'src/utils';
|
||||
|
||||
test.describe('Album', () => {
|
||||
let admin: LoginResponseDto;
|
||||
@@ -22,4 +23,41 @@ test.describe('Album', () => {
|
||||
await page.reload();
|
||||
await page.getByRole('button', { name: 'Select photos' }).waitFor();
|
||||
});
|
||||
|
||||
test('should keep map view open after viewing an asset from the map and going back', async ({ context, page }) => {
|
||||
await utils.setAuthCookies(context, admin.accessToken);
|
||||
|
||||
const imagePath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
|
||||
const mapAsset = await utils.createAsset(admin.accessToken, {
|
||||
assetData: {
|
||||
bytes: readFileSync(imagePath),
|
||||
filename: 'thompson-springs.jpg',
|
||||
},
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
|
||||
const mapAlbum = await utils.createAlbum(admin.accessToken, {
|
||||
albumName: 'Map Test Album',
|
||||
assetIds: [mapAsset.id],
|
||||
});
|
||||
|
||||
await page.goto(`/albums/${mapAlbum.id}`);
|
||||
const mapButton = page.getByRole('button', { name: 'Map' });
|
||||
await expect(mapButton).toBeVisible();
|
||||
await mapButton.click();
|
||||
|
||||
const mapModal = page.getByRole('dialog');
|
||||
await expect(mapModal).toBeVisible();
|
||||
|
||||
const mapMarker = mapModal.getByRole('img', { name: /Map marker/i }).first();
|
||||
await expect(mapMarker).toBeVisible();
|
||||
await mapMarker.click();
|
||||
|
||||
await page.waitForSelector('#immich-asset-viewer');
|
||||
await page.getByRole('button', { name: 'Go back' }).click();
|
||||
|
||||
await expect(page.locator('#immich-asset-viewer')).not.toBeVisible();
|
||||
await expect(mapModal).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { LoginResponseDto } from '@immich/sdk';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { lookup } from 'node:dns/promises';
|
||||
import { playwrightHost } from 'playwright.config';
|
||||
import { utils } from 'src/utils';
|
||||
|
||||
test.describe('Websocket', () => {
|
||||
@@ -14,28 +12,14 @@ test.describe('Websocket', () => {
|
||||
});
|
||||
|
||||
test('connects using ipv4', async ({ page, context }) => {
|
||||
const { address: ipv4 } = await lookup(playwrightHost, 4);
|
||||
await utils.setAuthCookies(context, admin.accessToken, ipv4);
|
||||
await page.goto(`http://${ipv4}:2285/`);
|
||||
await utils.setAuthCookies(context, admin.accessToken);
|
||||
await page.goto('http://127.0.0.1:2285/');
|
||||
await expect(page.locator('#sidebar')).toContainText('Server Online');
|
||||
});
|
||||
|
||||
test('connects using ipv6', async ({ page, context }) => {
|
||||
let ipv6: string;
|
||||
if (playwrightHost === '127.0.0.1') {
|
||||
ipv6 = '::1';
|
||||
} else {
|
||||
try {
|
||||
const { address } = await lookup(playwrightHost, 6);
|
||||
ipv6 = address;
|
||||
} catch {
|
||||
test.skip(true, 'No IPv6 address available');
|
||||
return;
|
||||
}
|
||||
}
|
||||
const ipv6Url = `http://[${ipv6}]:2285/`;
|
||||
await utils.setAuthCookies(context, admin.accessToken, undefined, ipv6Url);
|
||||
await page.goto(ipv6Url);
|
||||
await utils.setAuthCookies(context, admin.accessToken, '[::1]');
|
||||
await page.goto('http://[::1]:2285/');
|
||||
await expect(page.locator('#sidebar')).toContainText('Server Online');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BrowserContext, expect, Page } from '@playwright/test';
|
||||
import { BrowserContext } from '@playwright/test';
|
||||
import { playwrightHost } from 'playwright.config';
|
||||
|
||||
export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserId: string) => {
|
||||
@@ -283,13 +283,3 @@ export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserI
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const waitForServiceWorker = async (page: Page) => {
|
||||
await expect
|
||||
.poll(() => page.context().serviceWorkers().length, {
|
||||
message:
|
||||
'Service worker not registered. Ensure the origin is a secure context (localhost or use --unsafely-treat-insecure-origin-as-secure flag).',
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeGreaterThan(0);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { AssetResponseDto } from '@immich/sdk';
|
||||
import { expect, Page } from '@playwright/test';
|
||||
import { waitForServiceWorker } from 'src/ui/mock-network/base-network';
|
||||
|
||||
function getAssetIdFromUrl(url: URL): string | null {
|
||||
const pathMatch = url.pathname.match(/\/memory\/photos\/([^/]+)/);
|
||||
@@ -16,7 +15,6 @@ export const memoryViewerUtils = {
|
||||
},
|
||||
|
||||
async waitForMemoryLoad(page: Page) {
|
||||
await waitForServiceWorker(page);
|
||||
await expect(this.locator(page)).toBeVisible();
|
||||
await expect(page.locator('#memory-viewer img').first()).toBeVisible();
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { BrowserContext, expect, Page } from '@playwright/test';
|
||||
import { DateTime } from 'luxon';
|
||||
import { TimelineAssetConfig } from 'src/ui/generators/timeline';
|
||||
import { waitForServiceWorker } from 'src/ui/mock-network/base-network';
|
||||
|
||||
export const sleep = (ms: number) => {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
@@ -144,7 +143,6 @@ export const timelineUtils = {
|
||||
return page.locator('#asset-grid');
|
||||
},
|
||||
async waitForTimelineLoad(page: Page) {
|
||||
await waitForServiceWorker(page);
|
||||
await expect(timelineUtils.locator(page)).toBeInViewport();
|
||||
await expect.poll(() => thumbnailUtils.locator(page).count()).toBeGreaterThan(0);
|
||||
},
|
||||
@@ -165,7 +163,6 @@ export const assetViewerUtils = {
|
||||
return page.locator('#immich-asset-viewer');
|
||||
},
|
||||
async waitForViewerLoad(page: Page, asset: TimelineAssetConfig) {
|
||||
await waitForServiceWorker(page);
|
||||
await page
|
||||
.locator(
|
||||
`img[draggable="false"][src="/api/assets/${asset.id}/thumbnail?size=preview&c=${asset.thumbhash}&edited=true"]`,
|
||||
|
||||
+23
-9
@@ -71,7 +71,7 @@ import { io, type Socket } from 'socket.io-client';
|
||||
import { loginDto, signupDto } from 'src/fixtures';
|
||||
import { makeRandomImage } from 'src/generators';
|
||||
import request from 'supertest';
|
||||
import { playwrightDbHost, playwrightDbPort, playwrightHost, playwriteBaseUrl } from '../playwright.config';
|
||||
import { playwrightDbHost, playwrightHost, playwriteBaseUrl } from '../playwright.config';
|
||||
|
||||
export type { Emitter } from '@socket.io/component-emitter';
|
||||
|
||||
@@ -81,7 +81,7 @@ type WaitOptions = { event: EventType; id?: string; total?: number; timeout?: nu
|
||||
type AdminSetupOptions = { onboarding?: boolean };
|
||||
type FileData = { bytes?: Buffer; filename: string };
|
||||
|
||||
const dbUrl = `postgres://postgres:postgres@${playwrightDbHost}:${playwrightDbPort}/immich`;
|
||||
const dbUrl = `postgres://postgres:postgres@${playwrightDbHost}:5435/immich`;
|
||||
export const baseUrl = playwriteBaseUrl;
|
||||
export const shareUrl = `${baseUrl}/share`;
|
||||
export const app = `${baseUrl}/api`;
|
||||
@@ -510,6 +510,9 @@ export const utils = {
|
||||
createStack: (accessToken: string, assetIds: string[]) =>
|
||||
createStack({ stackCreateDto: { assetIds } }, { headers: asBearerAuth(accessToken) }),
|
||||
|
||||
setAssetDuplicateId: (accessToken: string, assetId: string, duplicateId: string | null) =>
|
||||
updateAssets({ assetBulkUpdateDto: { ids: [assetId], duplicateId } }, { headers: asBearerAuth(accessToken) }),
|
||||
|
||||
upsertTags: (accessToken: string, tags: string[]) =>
|
||||
upsertTags({ tagUpsertDto: { tags } }, { headers: asBearerAuth(accessToken) }),
|
||||
|
||||
@@ -522,13 +525,13 @@ export const utils = {
|
||||
queueCommand: async (accessToken: string, name: QueueName, queueCommandDto: QueueCommandDto) =>
|
||||
runQueueCommandLegacy({ name, queueCommandDto }, { headers: asBearerAuth(accessToken) }),
|
||||
|
||||
setAuthCookies: async (context: BrowserContext, accessToken: string, domain = playwrightHost, url?: string) => {
|
||||
const origin = url ? { url } : { domain, path: '/' };
|
||||
setAuthCookies: async (context: BrowserContext, accessToken: string, domain = playwrightHost) =>
|
||||
await context.addCookies([
|
||||
{
|
||||
name: 'immich_access_token',
|
||||
value: accessToken,
|
||||
...origin,
|
||||
domain,
|
||||
path: '/',
|
||||
expires: 2_058_028_213,
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
@@ -537,7 +540,8 @@ export const utils = {
|
||||
{
|
||||
name: 'immich_auth_type',
|
||||
value: 'password',
|
||||
...origin,
|
||||
domain,
|
||||
path: '/',
|
||||
expires: 2_058_028_213,
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
@@ -546,14 +550,14 @@ export const utils = {
|
||||
{
|
||||
name: 'immich_is_authenticated',
|
||||
value: 'true',
|
||||
...origin,
|
||||
domain,
|
||||
path: '/',
|
||||
expires: 2_058_028_213,
|
||||
httpOnly: false,
|
||||
secure: false,
|
||||
sameSite: 'Lax',
|
||||
},
|
||||
]);
|
||||
},
|
||||
]),
|
||||
|
||||
setMaintenanceAuthCookie: async (context: BrowserContext, token: string, domain = '127.0.0.1') =>
|
||||
await context.addCookies([
|
||||
@@ -696,6 +700,16 @@ export const utils = {
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
downloadAsset: async (accessToken: string, id: string) => {
|
||||
const downloadedRes = await fetch(`${baseUrl}/api/assets/${id}/original`, {
|
||||
headers: asBearerAuth(accessToken),
|
||||
});
|
||||
if (!downloadedRes.ok) {
|
||||
throw new Error(`Failed to download asset ${id}: ${downloadedRes.status} ${await downloadedRes.text()}`);
|
||||
}
|
||||
return await downloadedRes.blob();
|
||||
},
|
||||
};
|
||||
|
||||
utils.initSdk();
|
||||
|
||||
+8
-6
@@ -866,6 +866,7 @@
|
||||
"crop_aspect_ratio_fixed": "Fixed",
|
||||
"crop_aspect_ratio_free": "Free",
|
||||
"crop_aspect_ratio_original": "Original",
|
||||
"crop_aspect_ratio_square": "Square",
|
||||
"curated_object_page_title": "Things",
|
||||
"current_device": "Current device",
|
||||
"current_pin_code": "Current PIN code",
|
||||
@@ -880,7 +881,7 @@
|
||||
"daily_title_text_date": "E, MMM dd",
|
||||
"daily_title_text_date_year": "E, MMM dd, yyyy",
|
||||
"dark": "Dark",
|
||||
"dark_theme": "Toggle dark theme",
|
||||
"dark_theme": "Switch to dark theme",
|
||||
"date": "Date",
|
||||
"date_after": "Date after",
|
||||
"date_and_time": "Date and Time",
|
||||
@@ -891,10 +892,8 @@
|
||||
"day": "Day",
|
||||
"days": "Days",
|
||||
"deduplicate_all": "Deduplicate All",
|
||||
"deduplication_criteria_1": "Image size in bytes",
|
||||
"deduplication_criteria_2": "Count of EXIF data",
|
||||
"deduplication_info": "Deduplication Info",
|
||||
"deduplication_info_description": "To automatically preselect assets and remove duplicates in bulk, we look at:",
|
||||
"default_locale": "Default Locale",
|
||||
"default_locale_description": "Format dates and numbers based on your browser locale",
|
||||
"delete": "Delete",
|
||||
"delete_action_confirmation_message": "Are you sure you want to delete this asset? This action will move the asset to the server's trash and will prompt if you want to delete it locally",
|
||||
"delete_action_prompt": "{count} deleted",
|
||||
@@ -970,7 +969,7 @@
|
||||
"downloading_media": "Downloading media",
|
||||
"drop_files_to_upload": "Drop files anywhere to upload",
|
||||
"duplicates": "Duplicates",
|
||||
"duplicates_description": "Resolve each group by indicating which, if any, are duplicates",
|
||||
"duplicates_description": "Resolve each group by indicating which, if any, are duplicates.",
|
||||
"duration": "Duration",
|
||||
"edit": "Edit",
|
||||
"edit_album": "Edit album",
|
||||
@@ -1387,9 +1386,11 @@
|
||||
"library_page_sort_title": "Album title",
|
||||
"licenses": "Licenses",
|
||||
"light": "Light",
|
||||
"light_theme": "Switch to light theme",
|
||||
"like": "Like",
|
||||
"like_deleted": "Like deleted",
|
||||
"link_motion_video": "Link motion video",
|
||||
"link_to_docs": "For more information, refer to the <link>documentation</link>.",
|
||||
"link_to_oauth": "Link to OAuth",
|
||||
"linked_oauth_account": "Linked OAuth account",
|
||||
"list": "List",
|
||||
@@ -2394,6 +2395,7 @@
|
||||
"viewer_remove_from_stack": "Remove from Stack",
|
||||
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
||||
"viewer_unstack": "Un-Stack",
|
||||
"visibility": "Visibility",
|
||||
"visibility_changed": "Visibility changed for {count, plural, one {# person} other {# people}}",
|
||||
"visual": "Visual",
|
||||
"visual_builder": "Visual builder",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-i18n",
|
||||
"version": "2.6.2",
|
||||
"version": "2.6.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"format": "prettier --cache --check .",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "immich-ml"
|
||||
version = "2.6.2"
|
||||
version = "2.6.3"
|
||||
description = ""
|
||||
authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }]
|
||||
requires-python = ">=3.11,<4.0"
|
||||
|
||||
Generated
+1
-1
@@ -898,7 +898,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "immich-ml"
|
||||
version = "2.6.2"
|
||||
version = "2.6.3"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiocache" },
|
||||
|
||||
@@ -14,9 +14,9 @@ config_roots = [
|
||||
]
|
||||
|
||||
[tools]
|
||||
node = "24.13.1"
|
||||
node = "24.14.0"
|
||||
flutter = "3.35.7"
|
||||
pnpm = "10.30.3"
|
||||
pnpm = "10.32.1"
|
||||
terragrunt = "0.99.4"
|
||||
opentofu = "1.11.5"
|
||||
java = "21.0.2"
|
||||
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 3040,
|
||||
"android.injected.version.name" => "2.6.2",
|
||||
"android.injected.version.code" => 3041,
|
||||
"android.injected.version.name" => "2.6.3",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
@@ -150,7 +150,6 @@ class URLSessionManager: NSObject {
|
||||
config.httpCookieStorage = cookieStorage
|
||||
config.httpMaximumConnectionsPerHost = 64
|
||||
config.timeoutIntervalForRequest = 60
|
||||
config.timeoutIntervalForResource = 300
|
||||
|
||||
var headers = UserDefaults.group.dictionary(forKey: HEADERS_KEY) as? [String: String] ?? [:]
|
||||
headers["User-Agent"] = headers["User-Agent"] ?? userAgent
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.6.2</string>
|
||||
<string>2.6.3</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -207,6 +207,11 @@ class DriftMemoryPage extends HookConsumerWidget {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
DriftMemoryPage.setMemory(ref, memories[pageNumber]);
|
||||
});
|
||||
|
||||
// Update currentAsset to the first asset of the new memory
|
||||
if (memories[pageNumber].assets.isNotEmpty) {
|
||||
currentAsset.value = memories[pageNumber].assets.first;
|
||||
}
|
||||
}
|
||||
|
||||
currentAssetPage.value = 0;
|
||||
|
||||
@@ -71,16 +71,13 @@ class ViewerBottomBar extends ConsumerWidget {
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (asset.isVideo) VideoControls(videoPlayerName: asset.heroTag),
|
||||
if (!isReadonlyModeEnabled)
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (asset.isVideo) VideoControls(videoPlayerName: asset.heroTag),
|
||||
if (!isReadonlyModeEnabled)
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -3,24 +3,21 @@ import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart' show InformationCollector;
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/cache_aware_listener_tracker.mixin.dart';
|
||||
|
||||
/// A [MultiFrameImageStreamCompleter] with support for listener tracking
|
||||
/// which makes resource cleanup possible when no longer needed.
|
||||
/// Codec is disposed through the MultiFrameImageStreamCompleter's internals onDispose method
|
||||
class AnimatedImageStreamCompleter extends MultiFrameImageStreamCompleter {
|
||||
void Function()? _onLastListenerRemoved;
|
||||
int _listenerCount = 0;
|
||||
// True once any image or the codec has been provided.
|
||||
// Until then the image cache holds one listener, so "last real listener gone"
|
||||
// is _listenerCount == 1, not 0.
|
||||
bool didProvideImage = false;
|
||||
|
||||
class AnimatedImageStreamCompleter extends MultiFrameImageStreamCompleter with CacheAwareListenerTrackerMixin {
|
||||
AnimatedImageStreamCompleter._({
|
||||
required super.codec,
|
||||
required super.scale,
|
||||
required bool hadInitialImage,
|
||||
super.informationCollector,
|
||||
void Function()? onLastListenerRemoved,
|
||||
}) : _onLastListenerRemoved = onLastListenerRemoved;
|
||||
}) {
|
||||
setupListenerTracking(hadInitialImage: hadInitialImage, onLastListenerRemoved: onLastListenerRemoved);
|
||||
}
|
||||
|
||||
factory AnimatedImageStreamCompleter({
|
||||
required Stream<Object> stream,
|
||||
@@ -33,23 +30,21 @@ class AnimatedImageStreamCompleter extends MultiFrameImageStreamCompleter {
|
||||
final self = AnimatedImageStreamCompleter._(
|
||||
codec: codecCompleter.future,
|
||||
scale: scale,
|
||||
hadInitialImage: initialImage != null,
|
||||
informationCollector: informationCollector,
|
||||
onLastListenerRemoved: onLastListenerRemoved,
|
||||
);
|
||||
|
||||
if (initialImage != null) {
|
||||
self.didProvideImage = true;
|
||||
self.setImage(initialImage);
|
||||
}
|
||||
|
||||
stream.listen(
|
||||
(item) {
|
||||
if (item is ImageInfo) {
|
||||
self.didProvideImage = true;
|
||||
self.setImage(item);
|
||||
} else if (item is ui.Codec) {
|
||||
if (!codecCompleter.isCompleted) {
|
||||
self.didProvideImage = true;
|
||||
codecCompleter.complete(item);
|
||||
}
|
||||
}
|
||||
@@ -70,27 +65,4 @@ class AnimatedImageStreamCompleter extends MultiFrameImageStreamCompleter {
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@override
|
||||
void addListener(ImageStreamListener listener) {
|
||||
super.addListener(listener);
|
||||
_listenerCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
void removeListener(ImageStreamListener listener) {
|
||||
super.removeListener(listener);
|
||||
_listenerCount--;
|
||||
|
||||
final bool onlyCacheListenerLeft = _listenerCount == 1 && !didProvideImage;
|
||||
final bool noListenersAfterCodec = _listenerCount == 0 && didProvideImage;
|
||||
|
||||
if (onlyCacheListenerLeft || noListenersAfterCodec) {
|
||||
final onLastListenerRemoved = _onLastListenerRemoved;
|
||||
if (onLastListenerRemoved != null) {
|
||||
_onLastListenerRemoved = null;
|
||||
onLastListenerRemoved();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
/// Tracks listeners on an [ImageStreamCompleter] to safely cancel in-flight
|
||||
/// network requests without interfering with [ImageCache] internals.
|
||||
///
|
||||
/// ### Problem
|
||||
/// Cancelling fetches when the listener count drops to 1 (cache only) or 0
|
||||
/// is unsafe due to three framework behaviours:
|
||||
///
|
||||
/// 1. **Memory-pressure eviction** — `ImageCache.clear()` removes the cache
|
||||
/// listener while UI widgets still need the image. A count-based check
|
||||
/// would cancel the active fetch, leaving the UI with no image.
|
||||
/// 2. **Synchronous detach during `putIfAbsent`** — When an `initialImage`
|
||||
/// is provided, the cache attaches, receives the frame, and detaches
|
||||
/// synchronously *before* the UI widget can attach. Count reaches 0 and
|
||||
/// would trigger a false cancel.
|
||||
/// 3. **Listener misidentification** — After the cache detaches (via 1 or 2),
|
||||
/// the next UI listener could be mistaken for the cache listener, causing
|
||||
/// incorrect cancellations when that widget is disposed.
|
||||
///
|
||||
/// ### Solution: First-Listener Heuristic
|
||||
/// The cache is always the first listener attached (via `putIfAbsent`). This
|
||||
/// mixin records that identity once and uses it for all subsequent decisions:
|
||||
///
|
||||
/// * **Identity locking** — The first listener is assumed to be the cache.
|
||||
/// Once identified, `_hasIdentifiedCacheListener` prevents reassignment.
|
||||
/// * **Targeted cancellation** — Cancel only when the identified cache
|
||||
/// listener is the sole remaining listener and no image has been delivered.
|
||||
/// * **Sync-removal bypass** — When `hadInitialImage` is set, the first
|
||||
/// synchronous removal of the cache listener is ignored so the fetch
|
||||
/// survives until the UI attaches.
|
||||
mixin CacheAwareListenerTrackerMixin on ImageStreamCompleter {
|
||||
void Function()? _onLastListenerRemoved;
|
||||
int _listenerCount = 0;
|
||||
bool _hadInitialImage = false;
|
||||
bool _hasIgnoredFirstSyncRemoval = false;
|
||||
ImageStreamListener? _cacheListener;
|
||||
bool _hasIdentifiedCacheListener = false;
|
||||
|
||||
/// Initializes the tracking state. Must be called in the subclass constructor.
|
||||
void setupListenerTracking({required bool hadInitialImage, void Function()? onLastListenerRemoved}) {
|
||||
_hadInitialImage = hadInitialImage;
|
||||
_onLastListenerRemoved = onLastListenerRemoved;
|
||||
}
|
||||
|
||||
@override
|
||||
void addListener(ImageStreamListener listener) {
|
||||
if (!_hasIdentifiedCacheListener) {
|
||||
_hasIdentifiedCacheListener = true;
|
||||
_cacheListener = listener;
|
||||
}
|
||||
|
||||
_listenerCount++;
|
||||
super.addListener(listener);
|
||||
}
|
||||
|
||||
@override
|
||||
void removeListener(ImageStreamListener listener) {
|
||||
super.removeListener(listener);
|
||||
_listenerCount--;
|
||||
|
||||
final bool isCacheListener = listener == _cacheListener;
|
||||
if (isCacheListener) {
|
||||
_cacheListener = null;
|
||||
}
|
||||
|
||||
if (_hadInitialImage && !_hasIgnoredFirstSyncRemoval && isCacheListener) {
|
||||
_hasIgnoredFirstSyncRemoval = true;
|
||||
return;
|
||||
}
|
||||
|
||||
final bool onlyCacheListenerLeft = _listenerCount == 1 && _cacheListener != null;
|
||||
|
||||
final bool completelyAbandoned = _listenerCount == 0;
|
||||
|
||||
if (onlyCacheListenerLeft || completelyAbandoned) {
|
||||
final onLastListenerRemoved = _onLastListenerRemoved;
|
||||
if (onLastListenerRemoved != null) {
|
||||
_onLastListenerRemoved = null;
|
||||
onLastListenerRemoved();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,10 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/cache_aware_listener_tracker.mixin.dart';
|
||||
|
||||
/// An ImageStreamCompleter with support for loading multiple images.
|
||||
class OneFramePlaceholderImageStreamCompleter extends ImageStreamCompleter {
|
||||
void Function()? _onLastListenerRemoved;
|
||||
int _listenerCount = 0;
|
||||
// True once setImage() has been called at least once.
|
||||
bool didProvideImage = false;
|
||||
|
||||
class OneFramePlaceholderImageStreamCompleter extends ImageStreamCompleter with CacheAwareListenerTrackerMixin {
|
||||
/// The constructor to create an OneFramePlaceholderImageStreamCompleter. The [images]
|
||||
/// should be the primary images to display (typically asynchronously as they load).
|
||||
/// The [initialImage] is an optional image that will be emitted synchronously
|
||||
@@ -24,14 +20,14 @@ class OneFramePlaceholderImageStreamCompleter extends ImageStreamCompleter {
|
||||
InformationCollector? informationCollector,
|
||||
void Function()? onLastListenerRemoved,
|
||||
}) {
|
||||
setupListenerTracking(hadInitialImage: initialImage != null, onLastListenerRemoved: onLastListenerRemoved);
|
||||
|
||||
if (initialImage != null) {
|
||||
didProvideImage = true;
|
||||
setImage(initialImage);
|
||||
}
|
||||
_onLastListenerRemoved = onLastListenerRemoved;
|
||||
|
||||
images.listen(
|
||||
(image) {
|
||||
didProvideImage = true;
|
||||
setImage(image);
|
||||
},
|
||||
onError: (Object error, StackTrace stack) {
|
||||
@@ -45,26 +41,4 @@ class OneFramePlaceholderImageStreamCompleter extends ImageStreamCompleter {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void addListener(ImageStreamListener listener) {
|
||||
super.addListener(listener);
|
||||
_listenerCount = _listenerCount + 1;
|
||||
}
|
||||
|
||||
@override
|
||||
void removeListener(ImageStreamListener listener) {
|
||||
super.removeListener(listener);
|
||||
_listenerCount = _listenerCount - 1;
|
||||
|
||||
final bool onlyCacheListenerLeft = _listenerCount == 1 && !didProvideImage;
|
||||
final bool noListenersAfterImage = _listenerCount == 0 && didProvideImage;
|
||||
|
||||
final onLastListenerRemoved = _onLastListenerRemoved;
|
||||
|
||||
if (onLastListenerRemoved != null && (noListenersAfterImage || onlyCacheListenerLeft)) {
|
||||
_onLastListenerRemoved = null;
|
||||
onLastListenerRemoved();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ class DownloadService {
|
||||
return result != null;
|
||||
} on PlatformException catch (error, stack) {
|
||||
// Handle saving MotionPhotos on iOS
|
||||
if (error.code == 'PHPhotosErrorDomain (-1)') {
|
||||
if (error.code.startsWith('PHPhotosErrorDomain')) {
|
||||
final result = await _fileMediaRepository.saveImageWithFile(imageFilePath, title: task.filename);
|
||||
return result != null;
|
||||
}
|
||||
|
||||
@@ -19,8 +19,16 @@ abstract final class DynamicTheme {
|
||||
// Some palettes do not generate surface container colors accurately,
|
||||
// so we regenerate all colors using the primary color
|
||||
_theme = ImmichTheme(
|
||||
light: ColorScheme.fromSeed(seedColor: primaryColor, brightness: Brightness.light),
|
||||
dark: ColorScheme.fromSeed(seedColor: primaryColor, brightness: Brightness.dark),
|
||||
light: ColorScheme.fromSeed(
|
||||
seedColor: primaryColor,
|
||||
brightness: Brightness.light,
|
||||
dynamicSchemeVariant: DynamicSchemeVariant.fidelity,
|
||||
),
|
||||
dark: ColorScheme.fromSeed(
|
||||
seedColor: primaryColor,
|
||||
brightness: Brightness.dark,
|
||||
dynamicSchemeVariant: DynamicSchemeVariant.fidelity,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -62,6 +62,7 @@ ThemeData getThemeData({required ColorScheme colorScheme, required Locale locale
|
||||
),
|
||||
chipTheme: const ChipThemeData(side: BorderSide.none),
|
||||
sliderTheme: const SliderThemeData(
|
||||
trackHeight: 12,
|
||||
// ignore: deprecated_member_use
|
||||
year2023: false,
|
||||
),
|
||||
|
||||
@@ -66,9 +66,9 @@ class VideoControls extends HookConsumerWidget {
|
||||
final isLoaded = duration != Duration.zero;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 12),
|
||||
child: Column(
|
||||
spacing: 16,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
@@ -77,8 +77,8 @@ class VideoControls extends HookConsumerWidget {
|
||||
padding: const EdgeInsets.all(12),
|
||||
constraints: const BoxConstraints(),
|
||||
icon: isFinished
|
||||
? const Icon(Icons.replay, color: Colors.white, size: 32, shadows: _controlShadows)
|
||||
: AnimatedPlayPause(color: Colors.white, size: 32, playing: isPlaying, shadows: _controlShadows),
|
||||
? const Icon(Icons.replay, color: Colors.white, shadows: _controlShadows)
|
||||
: AnimatedPlayPause(color: Colors.white, playing: isPlaying, shadows: _controlShadows),
|
||||
onPressed: () => _toggle(ref, isCasting),
|
||||
),
|
||||
const Spacer(),
|
||||
@@ -91,7 +91,7 @@ class VideoControls extends HookConsumerWidget {
|
||||
shadows: _controlShadows,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
|
||||
Generated
+11
-1
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 2.6.2
|
||||
- API version: 2.6.3
|
||||
- Generator version: 7.8.0
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
@@ -156,6 +156,7 @@ Class | Method | HTTP request | Description
|
||||
*DuplicatesApi* | [**deleteDuplicate**](doc//DuplicatesApi.md#deleteduplicate) | **DELETE** /duplicates/{id} | Delete a duplicate
|
||||
*DuplicatesApi* | [**deleteDuplicates**](doc//DuplicatesApi.md#deleteduplicates) | **DELETE** /duplicates | Delete duplicates
|
||||
*DuplicatesApi* | [**getAssetDuplicates**](doc//DuplicatesApi.md#getassetduplicates) | **GET** /duplicates | Retrieve duplicates
|
||||
*DuplicatesApi* | [**resolveDuplicates**](doc//DuplicatesApi.md#resolveduplicates) | **POST** /duplicates/resolve | Resolve duplicate groups
|
||||
*FacesApi* | [**createFace**](doc//FacesApi.md#createface) | **POST** /faces | Create a face
|
||||
*FacesApi* | [**deleteFace**](doc//FacesApi.md#deleteface) | **DELETE** /faces/{id} | Delete a face
|
||||
*FacesApi* | [**getFaces**](doc//FacesApi.md#getfaces) | **GET** /faces | Retrieve faces for asset
|
||||
@@ -293,6 +294,11 @@ Class | Method | HTTP request | Description
|
||||
*TrashApi* | [**emptyTrash**](doc//TrashApi.md#emptytrash) | **POST** /trash/empty | Empty trash
|
||||
*TrashApi* | [**restoreAssets**](doc//TrashApi.md#restoreassets) | **POST** /trash/restore/assets | Restore assets
|
||||
*TrashApi* | [**restoreTrash**](doc//TrashApi.md#restoretrash) | **POST** /trash/restore | Restore trash
|
||||
*UploadApi* | [**cancelUpload**](doc//UploadApi.md#cancelupload) | **DELETE** /upload/{id} |
|
||||
*UploadApi* | [**getUploadOptions**](doc//UploadApi.md#getuploadoptions) | **OPTIONS** /upload |
|
||||
*UploadApi* | [**getUploadStatus**](doc//UploadApi.md#getuploadstatus) | **HEAD** /upload/{id} |
|
||||
*UploadApi* | [**resumeUpload**](doc//UploadApi.md#resumeupload) | **PATCH** /upload/{id} |
|
||||
*UploadApi* | [**startUpload**](doc//UploadApi.md#startupload) | **POST** /upload |
|
||||
*UsersApi* | [**createProfileImage**](doc//UsersApi.md#createprofileimage) | **POST** /users/profile-image | Create user profile image
|
||||
*UsersApi* | [**deleteProfileImage**](doc//UsersApi.md#deleteprofileimage) | **DELETE** /users/profile-image | Delete user profile image
|
||||
*UsersApi* | [**deleteUserLicense**](doc//UsersApi.md#deleteuserlicense) | **DELETE** /users/me/license | Delete user product key
|
||||
@@ -422,6 +428,8 @@ Class | Method | HTTP request | Description
|
||||
- [DownloadResponseDto](doc//DownloadResponseDto.md)
|
||||
- [DownloadUpdate](doc//DownloadUpdate.md)
|
||||
- [DuplicateDetectionConfig](doc//DuplicateDetectionConfig.md)
|
||||
- [DuplicateResolveDto](doc//DuplicateResolveDto.md)
|
||||
- [DuplicateResolveGroupDto](doc//DuplicateResolveGroupDto.md)
|
||||
- [DuplicateResponseDto](doc//DuplicateResponseDto.md)
|
||||
- [EmailNotificationsResponse](doc//EmailNotificationsResponse.md)
|
||||
- [EmailNotificationsUpdate](doc//EmailNotificationsUpdate.md)
|
||||
@@ -655,6 +663,8 @@ Class | Method | HTTP request | Description
|
||||
- [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md)
|
||||
- [UpdateAssetDto](doc//UpdateAssetDto.md)
|
||||
- [UpdateLibraryDto](doc//UpdateLibraryDto.md)
|
||||
- [UploadBackupConfig](doc//UploadBackupConfig.md)
|
||||
- [UploadOkDto](doc//UploadOkDto.md)
|
||||
- [UsageByUserDto](doc//UsageByUserDto.md)
|
||||
- [UserAdminCreateDto](doc//UserAdminCreateDto.md)
|
||||
- [UserAdminDeleteDto](doc//UserAdminDeleteDto.md)
|
||||
|
||||
Generated
+5
@@ -63,6 +63,7 @@ part 'api/system_metadata_api.dart';
|
||||
part 'api/tags_api.dart';
|
||||
part 'api/timeline_api.dart';
|
||||
part 'api/trash_api.dart';
|
||||
part 'api/upload_api.dart';
|
||||
part 'api/users_api.dart';
|
||||
part 'api/users_admin_api.dart';
|
||||
part 'api/views_api.dart';
|
||||
@@ -161,6 +162,8 @@ part 'model/download_response.dart';
|
||||
part 'model/download_response_dto.dart';
|
||||
part 'model/download_update.dart';
|
||||
part 'model/duplicate_detection_config.dart';
|
||||
part 'model/duplicate_resolve_dto.dart';
|
||||
part 'model/duplicate_resolve_group_dto.dart';
|
||||
part 'model/duplicate_response_dto.dart';
|
||||
part 'model/email_notifications_response.dart';
|
||||
part 'model/email_notifications_update.dart';
|
||||
@@ -394,6 +397,8 @@ part 'model/update_album_dto.dart';
|
||||
part 'model/update_album_user_dto.dart';
|
||||
part 'model/update_asset_dto.dart';
|
||||
part 'model/update_library_dto.dart';
|
||||
part 'model/upload_backup_config.dart';
|
||||
part 'model/upload_ok_dto.dart';
|
||||
part 'model/usage_by_user_dto.dart';
|
||||
part 'model/user_admin_create_dto.dart';
|
||||
part 'model/user_admin_delete_dto.dart';
|
||||
|
||||
+59
@@ -163,4 +163,63 @@ class DuplicatesApi {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Resolve duplicate groups
|
||||
///
|
||||
/// Resolve duplicate groups by synchronizing metadata across assets and deleting/trashing duplicates.
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [DuplicateResolveDto] duplicateResolveDto (required):
|
||||
Future<Response> resolveDuplicatesWithHttpInfo(DuplicateResolveDto duplicateResolveDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/duplicates/resolve';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = duplicateResolveDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'POST',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Resolve duplicate groups
|
||||
///
|
||||
/// Resolve duplicate groups by synchronizing metadata across assets and deleting/trashing duplicates.
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [DuplicateResolveDto] duplicateResolveDto (required):
|
||||
Future<List<BulkIdResponseDto>?> resolveDuplicates(DuplicateResolveDto duplicateResolveDto,) async {
|
||||
final response = await resolveDuplicatesWithHttpInfo(duplicateResolveDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
final responseBody = await _decodeBodyBytes(response);
|
||||
return (await apiClient.deserializeAsync(responseBody, 'List<BulkIdResponseDto>') as List)
|
||||
.cast<BulkIdResponseDto>()
|
||||
.toList(growable: false);
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+359
@@ -0,0 +1,359 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
|
||||
class UploadApi {
|
||||
UploadApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
|
||||
|
||||
final ApiClient apiClient;
|
||||
|
||||
/// Performs an HTTP 'DELETE /upload/{id}' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] slug:
|
||||
Future<Response> cancelUploadWithHttpInfo(String id, { String? key, String? slug, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/upload/{id}'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (key != null) {
|
||||
queryParams.addAll(_queryParams('', 'key', key));
|
||||
}
|
||||
if (slug != null) {
|
||||
queryParams.addAll(_queryParams('', 'slug', slug));
|
||||
}
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'DELETE',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] slug:
|
||||
Future<void> cancelUpload(String id, { String? key, String? slug, }) async {
|
||||
final response = await cancelUploadWithHttpInfo(id, key: key, slug: slug, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'OPTIONS /upload' operation and returns the [Response].
|
||||
Future<Response> getUploadOptionsWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/upload';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'OPTIONS',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> getUploadOptions() async {
|
||||
final response = await getUploadOptionsWithHttpInfo();
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'HEAD /upload/{id}' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [String] uploadDraftInteropVersion (required):
|
||||
/// Indicates the version of the RUFH protocol supported by the client.
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] slug:
|
||||
Future<Response> getUploadStatusWithHttpInfo(String id, String uploadDraftInteropVersion, { String? key, String? slug, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/upload/{id}'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (key != null) {
|
||||
queryParams.addAll(_queryParams('', 'key', key));
|
||||
}
|
||||
if (slug != null) {
|
||||
queryParams.addAll(_queryParams('', 'slug', slug));
|
||||
}
|
||||
|
||||
headerParams[r'upload-draft-interop-version'] = parameterToString(uploadDraftInteropVersion);
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'HEAD',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [String] uploadDraftInteropVersion (required):
|
||||
/// Indicates the version of the RUFH protocol supported by the client.
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] slug:
|
||||
Future<void> getUploadStatus(String id, String uploadDraftInteropVersion, { String? key, String? slug, }) async {
|
||||
final response = await getUploadStatusWithHttpInfo(id, uploadDraftInteropVersion, key: key, slug: slug, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'PATCH /upload/{id}' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] contentLength (required):
|
||||
/// Non-negative size of the request body in bytes.
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [String] uploadComplete (required):
|
||||
/// Structured boolean indicating whether this request completes the file. Use Upload-Incomplete instead for version <= 3.
|
||||
///
|
||||
/// * [String] uploadDraftInteropVersion (required):
|
||||
/// Indicates the version of the RUFH protocol supported by the client.
|
||||
///
|
||||
/// * [String] uploadOffset (required):
|
||||
/// Non-negative byte offset indicating the starting position of the data in the request body within the entire file.
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] slug:
|
||||
Future<Response> resumeUploadWithHttpInfo(String contentLength, String id, String uploadComplete, String uploadDraftInteropVersion, String uploadOffset, { String? key, String? slug, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/upload/{id}'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (key != null) {
|
||||
queryParams.addAll(_queryParams('', 'key', key));
|
||||
}
|
||||
if (slug != null) {
|
||||
queryParams.addAll(_queryParams('', 'slug', slug));
|
||||
}
|
||||
|
||||
headerParams[r'content-length'] = parameterToString(contentLength);
|
||||
headerParams[r'upload-complete'] = parameterToString(uploadComplete);
|
||||
headerParams[r'upload-draft-interop-version'] = parameterToString(uploadDraftInteropVersion);
|
||||
headerParams[r'upload-offset'] = parameterToString(uploadOffset);
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'PATCH',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] contentLength (required):
|
||||
/// Non-negative size of the request body in bytes.
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [String] uploadComplete (required):
|
||||
/// Structured boolean indicating whether this request completes the file. Use Upload-Incomplete instead for version <= 3.
|
||||
///
|
||||
/// * [String] uploadDraftInteropVersion (required):
|
||||
/// Indicates the version of the RUFH protocol supported by the client.
|
||||
///
|
||||
/// * [String] uploadOffset (required):
|
||||
/// Non-negative byte offset indicating the starting position of the data in the request body within the entire file.
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] slug:
|
||||
Future<UploadOkDto?> resumeUpload(String contentLength, String id, String uploadComplete, String uploadDraftInteropVersion, String uploadOffset, { String? key, String? slug, }) async {
|
||||
final response = await resumeUploadWithHttpInfo(contentLength, id, uploadComplete, uploadDraftInteropVersion, uploadOffset, key: key, slug: slug, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UploadOkDto',) as UploadOkDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'POST /upload' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] contentLength (required):
|
||||
/// Non-negative size of the request body in bytes.
|
||||
///
|
||||
/// * [String] reprDigest (required):
|
||||
/// RFC 9651 structured dictionary containing an `sha` (bytesequence) checksum used to detect duplicate files and validate data integrity.
|
||||
///
|
||||
/// * [String] xImmichAssetData (required):
|
||||
/// RFC 9651 structured dictionary containing asset metadata with the following keys: - device-asset-id (string, required): Unique device asset identifier - device-id (string, required): Device identifier - file-created-at (string/date, required): ISO 8601 date string or Unix timestamp - file-modified-at (string/date, required): ISO 8601 date string or Unix timestamp - filename (string, required): Original filename - is-favorite (boolean, optional): Favorite status - live-photo-video-id (string, optional): Live photo ID for assets from iOS devices - icloud-id (string, optional): iCloud identifier for assets from iOS devices
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] slug:
|
||||
///
|
||||
/// * [String] uploadComplete:
|
||||
/// Structured boolean indicating whether this request completes the file. Use Upload-Incomplete instead for version <= 3.
|
||||
///
|
||||
/// * [String] uploadDraftInteropVersion:
|
||||
/// Indicates the version of the RUFH protocol supported by the client.
|
||||
Future<Response> startUploadWithHttpInfo(String contentLength, String reprDigest, String xImmichAssetData, { String? key, String? slug, String? uploadComplete, String? uploadDraftInteropVersion, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/upload';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (key != null) {
|
||||
queryParams.addAll(_queryParams('', 'key', key));
|
||||
}
|
||||
if (slug != null) {
|
||||
queryParams.addAll(_queryParams('', 'slug', slug));
|
||||
}
|
||||
|
||||
headerParams[r'content-length'] = parameterToString(contentLength);
|
||||
headerParams[r'repr-digest'] = parameterToString(reprDigest);
|
||||
if (uploadComplete != null) {
|
||||
headerParams[r'upload-complete'] = parameterToString(uploadComplete);
|
||||
}
|
||||
if (uploadDraftInteropVersion != null) {
|
||||
headerParams[r'upload-draft-interop-version'] = parameterToString(uploadDraftInteropVersion);
|
||||
}
|
||||
headerParams[r'x-immich-asset-data'] = parameterToString(xImmichAssetData);
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'POST',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] contentLength (required):
|
||||
/// Non-negative size of the request body in bytes.
|
||||
///
|
||||
/// * [String] reprDigest (required):
|
||||
/// RFC 9651 structured dictionary containing an `sha` (bytesequence) checksum used to detect duplicate files and validate data integrity.
|
||||
///
|
||||
/// * [String] xImmichAssetData (required):
|
||||
/// RFC 9651 structured dictionary containing asset metadata with the following keys: - device-asset-id (string, required): Unique device asset identifier - device-id (string, required): Device identifier - file-created-at (string/date, required): ISO 8601 date string or Unix timestamp - file-modified-at (string/date, required): ISO 8601 date string or Unix timestamp - filename (string, required): Original filename - is-favorite (boolean, optional): Favorite status - live-photo-video-id (string, optional): Live photo ID for assets from iOS devices - icloud-id (string, optional): iCloud identifier for assets from iOS devices
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] slug:
|
||||
///
|
||||
/// * [String] uploadComplete:
|
||||
/// Structured boolean indicating whether this request completes the file. Use Upload-Incomplete instead for version <= 3.
|
||||
///
|
||||
/// * [String] uploadDraftInteropVersion:
|
||||
/// Indicates the version of the RUFH protocol supported by the client.
|
||||
Future<UploadOkDto?> startUpload(String contentLength, String reprDigest, String xImmichAssetData, { String? key, String? slug, String? uploadComplete, String? uploadDraftInteropVersion, }) async {
|
||||
final response = await startUploadWithHttpInfo(contentLength, reprDigest, xImmichAssetData, key: key, slug: slug, uploadComplete: uploadComplete, uploadDraftInteropVersion: uploadDraftInteropVersion, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UploadOkDto',) as UploadOkDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Generated
+8
@@ -368,6 +368,10 @@ class ApiClient {
|
||||
return DownloadUpdate.fromJson(value);
|
||||
case 'DuplicateDetectionConfig':
|
||||
return DuplicateDetectionConfig.fromJson(value);
|
||||
case 'DuplicateResolveDto':
|
||||
return DuplicateResolveDto.fromJson(value);
|
||||
case 'DuplicateResolveGroupDto':
|
||||
return DuplicateResolveGroupDto.fromJson(value);
|
||||
case 'DuplicateResponseDto':
|
||||
return DuplicateResponseDto.fromJson(value);
|
||||
case 'EmailNotificationsResponse':
|
||||
@@ -834,6 +838,10 @@ class ApiClient {
|
||||
return UpdateAssetDto.fromJson(value);
|
||||
case 'UpdateLibraryDto':
|
||||
return UpdateLibraryDto.fromJson(value);
|
||||
case 'UploadBackupConfig':
|
||||
return UploadBackupConfig.fromJson(value);
|
||||
case 'UploadOkDto':
|
||||
return UploadOkDto.fromJson(value);
|
||||
case 'UsageByUserDto':
|
||||
return UsageByUserDto.fromJson(value);
|
||||
case 'UserAdminCreateDto':
|
||||
|
||||
@@ -27,6 +27,7 @@ class BulkIdErrorReason {
|
||||
static const noPermission = BulkIdErrorReason._(r'no_permission');
|
||||
static const notFound = BulkIdErrorReason._(r'not_found');
|
||||
static const unknown = BulkIdErrorReason._(r'unknown');
|
||||
static const validation = BulkIdErrorReason._(r'validation');
|
||||
|
||||
/// List of all possible values in this [enum][BulkIdErrorReason].
|
||||
static const values = <BulkIdErrorReason>[
|
||||
@@ -34,6 +35,7 @@ class BulkIdErrorReason {
|
||||
noPermission,
|
||||
notFound,
|
||||
unknown,
|
||||
validation,
|
||||
];
|
||||
|
||||
static BulkIdErrorReason? fromJson(dynamic value) => BulkIdErrorReasonTypeTransformer().decode(value);
|
||||
@@ -76,6 +78,7 @@ class BulkIdErrorReasonTypeTransformer {
|
||||
case r'no_permission': return BulkIdErrorReason.noPermission;
|
||||
case r'not_found': return BulkIdErrorReason.notFound;
|
||||
case r'unknown': return BulkIdErrorReason.unknown;
|
||||
case r'validation': return BulkIdErrorReason.validation;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
|
||||
+21
-1
@@ -14,6 +14,7 @@ class BulkIdResponseDto {
|
||||
/// Returns a new [BulkIdResponseDto] instance.
|
||||
BulkIdResponseDto({
|
||||
this.error,
|
||||
this.errorMessage,
|
||||
required this.id,
|
||||
required this.success,
|
||||
});
|
||||
@@ -21,6 +22,14 @@ class BulkIdResponseDto {
|
||||
/// Error reason if failed
|
||||
BulkIdResponseDtoErrorEnum? error;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? errorMessage;
|
||||
|
||||
/// ID
|
||||
String id;
|
||||
|
||||
@@ -30,6 +39,7 @@ class BulkIdResponseDto {
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is BulkIdResponseDto &&
|
||||
other.error == error &&
|
||||
other.errorMessage == errorMessage &&
|
||||
other.id == id &&
|
||||
other.success == success;
|
||||
|
||||
@@ -37,11 +47,12 @@ class BulkIdResponseDto {
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(error == null ? 0 : error!.hashCode) +
|
||||
(errorMessage == null ? 0 : errorMessage!.hashCode) +
|
||||
(id.hashCode) +
|
||||
(success.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'BulkIdResponseDto[error=$error, id=$id, success=$success]';
|
||||
String toString() => 'BulkIdResponseDto[error=$error, errorMessage=$errorMessage, id=$id, success=$success]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -49,6 +60,11 @@ class BulkIdResponseDto {
|
||||
json[r'error'] = this.error;
|
||||
} else {
|
||||
// json[r'error'] = null;
|
||||
}
|
||||
if (this.errorMessage != null) {
|
||||
json[r'errorMessage'] = this.errorMessage;
|
||||
} else {
|
||||
// json[r'errorMessage'] = null;
|
||||
}
|
||||
json[r'id'] = this.id;
|
||||
json[r'success'] = this.success;
|
||||
@@ -65,6 +81,7 @@ class BulkIdResponseDto {
|
||||
|
||||
return BulkIdResponseDto(
|
||||
error: BulkIdResponseDtoErrorEnum.fromJson(json[r'error']),
|
||||
errorMessage: mapValueOfType<String>(json, r'errorMessage'),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
success: mapValueOfType<bool>(json, r'success')!,
|
||||
);
|
||||
@@ -136,6 +153,7 @@ class BulkIdResponseDtoErrorEnum {
|
||||
static const noPermission = BulkIdResponseDtoErrorEnum._(r'no_permission');
|
||||
static const notFound = BulkIdResponseDtoErrorEnum._(r'not_found');
|
||||
static const unknown = BulkIdResponseDtoErrorEnum._(r'unknown');
|
||||
static const validation = BulkIdResponseDtoErrorEnum._(r'validation');
|
||||
|
||||
/// List of all possible values in this [enum][BulkIdResponseDtoErrorEnum].
|
||||
static const values = <BulkIdResponseDtoErrorEnum>[
|
||||
@@ -143,6 +161,7 @@ class BulkIdResponseDtoErrorEnum {
|
||||
noPermission,
|
||||
notFound,
|
||||
unknown,
|
||||
validation,
|
||||
];
|
||||
|
||||
static BulkIdResponseDtoErrorEnum? fromJson(dynamic value) => BulkIdResponseDtoErrorEnumTypeTransformer().decode(value);
|
||||
@@ -185,6 +204,7 @@ class BulkIdResponseDtoErrorEnumTypeTransformer {
|
||||
case r'no_permission': return BulkIdResponseDtoErrorEnum.noPermission;
|
||||
case r'not_found': return BulkIdResponseDtoErrorEnum.notFound;
|
||||
case r'unknown': return BulkIdResponseDtoErrorEnum.unknown;
|
||||
case r'validation': return BulkIdResponseDtoErrorEnum.validation;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class DuplicateResolveDto {
|
||||
/// Returns a new [DuplicateResolveDto] instance.
|
||||
DuplicateResolveDto({
|
||||
this.groups = const [],
|
||||
});
|
||||
|
||||
/// List of duplicate groups to resolve
|
||||
List<DuplicateResolveGroupDto> groups;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is DuplicateResolveDto &&
|
||||
_deepEquality.equals(other.groups, groups);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(groups.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'DuplicateResolveDto[groups=$groups]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'groups'] = this.groups;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [DuplicateResolveDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static DuplicateResolveDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "DuplicateResolveDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return DuplicateResolveDto(
|
||||
groups: DuplicateResolveGroupDto.listFromJson(json[r'groups']),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<DuplicateResolveDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <DuplicateResolveDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = DuplicateResolveDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, DuplicateResolveDto> mapFromJson(dynamic json) {
|
||||
final map = <String, DuplicateResolveDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = DuplicateResolveDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of DuplicateResolveDto-objects as value to a dart map
|
||||
static Map<String, List<DuplicateResolveDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<DuplicateResolveDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = DuplicateResolveDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'groups',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class DuplicateResolveGroupDto {
|
||||
/// Returns a new [DuplicateResolveGroupDto] instance.
|
||||
DuplicateResolveGroupDto({
|
||||
required this.duplicateId,
|
||||
this.keepAssetIds = const [],
|
||||
this.trashAssetIds = const [],
|
||||
});
|
||||
|
||||
String duplicateId;
|
||||
|
||||
/// Asset IDs to keep
|
||||
List<String> keepAssetIds;
|
||||
|
||||
/// Asset IDs to trash or delete
|
||||
List<String> trashAssetIds;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is DuplicateResolveGroupDto &&
|
||||
other.duplicateId == duplicateId &&
|
||||
_deepEquality.equals(other.keepAssetIds, keepAssetIds) &&
|
||||
_deepEquality.equals(other.trashAssetIds, trashAssetIds);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(duplicateId.hashCode) +
|
||||
(keepAssetIds.hashCode) +
|
||||
(trashAssetIds.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'DuplicateResolveGroupDto[duplicateId=$duplicateId, keepAssetIds=$keepAssetIds, trashAssetIds=$trashAssetIds]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'duplicateId'] = this.duplicateId;
|
||||
json[r'keepAssetIds'] = this.keepAssetIds;
|
||||
json[r'trashAssetIds'] = this.trashAssetIds;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [DuplicateResolveGroupDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static DuplicateResolveGroupDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "DuplicateResolveGroupDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return DuplicateResolveGroupDto(
|
||||
duplicateId: mapValueOfType<String>(json, r'duplicateId')!,
|
||||
keepAssetIds: json[r'keepAssetIds'] is Iterable
|
||||
? (json[r'keepAssetIds'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
trashAssetIds: json[r'trashAssetIds'] is Iterable
|
||||
? (json[r'trashAssetIds'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<DuplicateResolveGroupDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <DuplicateResolveGroupDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = DuplicateResolveGroupDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, DuplicateResolveGroupDto> mapFromJson(dynamic json) {
|
||||
final map = <String, DuplicateResolveGroupDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = DuplicateResolveGroupDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of DuplicateResolveGroupDto-objects as value to a dart map
|
||||
static Map<String, List<DuplicateResolveGroupDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<DuplicateResolveGroupDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = DuplicateResolveGroupDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'duplicateId',
|
||||
'keepAssetIds',
|
||||
'trashAssetIds',
|
||||
};
|
||||
}
|
||||
|
||||
+14
-3
@@ -15,6 +15,7 @@ class DuplicateResponseDto {
|
||||
DuplicateResponseDto({
|
||||
this.assets = const [],
|
||||
required this.duplicateId,
|
||||
this.suggestedKeepAssetIds = const [],
|
||||
});
|
||||
|
||||
/// Duplicate assets
|
||||
@@ -23,24 +24,30 @@ class DuplicateResponseDto {
|
||||
/// Duplicate group ID
|
||||
String duplicateId;
|
||||
|
||||
/// Suggested asset IDs to keep based on file size and EXIF data
|
||||
List<String> suggestedKeepAssetIds;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is DuplicateResponseDto &&
|
||||
_deepEquality.equals(other.assets, assets) &&
|
||||
other.duplicateId == duplicateId;
|
||||
other.duplicateId == duplicateId &&
|
||||
_deepEquality.equals(other.suggestedKeepAssetIds, suggestedKeepAssetIds);
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(assets.hashCode) +
|
||||
(duplicateId.hashCode);
|
||||
(duplicateId.hashCode) +
|
||||
(suggestedKeepAssetIds.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'DuplicateResponseDto[assets=$assets, duplicateId=$duplicateId]';
|
||||
String toString() => 'DuplicateResponseDto[assets=$assets, duplicateId=$duplicateId, suggestedKeepAssetIds=$suggestedKeepAssetIds]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'assets'] = this.assets;
|
||||
json[r'duplicateId'] = this.duplicateId;
|
||||
json[r'suggestedKeepAssetIds'] = this.suggestedKeepAssetIds;
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -55,6 +62,9 @@ class DuplicateResponseDto {
|
||||
return DuplicateResponseDto(
|
||||
assets: AssetResponseDto.listFromJson(json[r'assets']),
|
||||
duplicateId: mapValueOfType<String>(json, r'duplicateId')!,
|
||||
suggestedKeepAssetIds: json[r'suggestedKeepAssetIds'] is Iterable
|
||||
? (json[r'suggestedKeepAssetIds'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -104,6 +114,7 @@ class DuplicateResponseDto {
|
||||
static const requiredKeys = <String>{
|
||||
'assets',
|
||||
'duplicateId',
|
||||
'suggestedKeepAssetIds',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Generated
+6
@@ -38,6 +38,8 @@ class JobName {
|
||||
static const assetFileMigration = JobName._(r'AssetFileMigration');
|
||||
static const assetGenerateThumbnailsQueueAll = JobName._(r'AssetGenerateThumbnailsQueueAll');
|
||||
static const assetGenerateThumbnails = JobName._(r'AssetGenerateThumbnails');
|
||||
static const partialAssetCleanup = JobName._(r'PartialAssetCleanup');
|
||||
static const partialAssetCleanupQueueAll = JobName._(r'PartialAssetCleanupQueueAll');
|
||||
static const auditLogCleanup = JobName._(r'AuditLogCleanup');
|
||||
static const auditTableCleanup = JobName._(r'AuditTableCleanup');
|
||||
static const databaseBackup = JobName._(r'DatabaseBackup');
|
||||
@@ -97,6 +99,8 @@ class JobName {
|
||||
assetFileMigration,
|
||||
assetGenerateThumbnailsQueueAll,
|
||||
assetGenerateThumbnails,
|
||||
partialAssetCleanup,
|
||||
partialAssetCleanupQueueAll,
|
||||
auditLogCleanup,
|
||||
auditTableCleanup,
|
||||
databaseBackup,
|
||||
@@ -191,6 +195,8 @@ class JobNameTypeTransformer {
|
||||
case r'AssetFileMigration': return JobName.assetFileMigration;
|
||||
case r'AssetGenerateThumbnailsQueueAll': return JobName.assetGenerateThumbnailsQueueAll;
|
||||
case r'AssetGenerateThumbnails': return JobName.assetGenerateThumbnails;
|
||||
case r'PartialAssetCleanup': return JobName.partialAssetCleanup;
|
||||
case r'PartialAssetCleanupQueueAll': return JobName.partialAssetCleanupQueueAll;
|
||||
case r'AuditLogCleanup': return JobName.auditLogCleanup;
|
||||
case r'AuditTableCleanup': return JobName.auditTableCleanup;
|
||||
case r'DatabaseBackup': return JobName.databaseBackup;
|
||||
|
||||
+1
-1
@@ -379,7 +379,7 @@ class MetadataSearchDto {
|
||||
///
|
||||
bool? withExif;
|
||||
|
||||
/// Include assets with people
|
||||
/// Include people data in response
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
|
||||
+1
-1
@@ -273,7 +273,7 @@ class RandomSearchDto {
|
||||
///
|
||||
bool? withExif;
|
||||
|
||||
/// Include assets with people
|
||||
/// Include people data in response
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
|
||||
+11
-3
@@ -14,25 +14,31 @@ class SystemConfigBackupsDto {
|
||||
/// Returns a new [SystemConfigBackupsDto] instance.
|
||||
SystemConfigBackupsDto({
|
||||
required this.database,
|
||||
required this.upload,
|
||||
});
|
||||
|
||||
DatabaseBackupConfig database;
|
||||
|
||||
UploadBackupConfig upload;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigBackupsDto &&
|
||||
other.database == database;
|
||||
other.database == database &&
|
||||
other.upload == upload;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(database.hashCode);
|
||||
(database.hashCode) +
|
||||
(upload.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SystemConfigBackupsDto[database=$database]';
|
||||
String toString() => 'SystemConfigBackupsDto[database=$database, upload=$upload]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'database'] = this.database;
|
||||
json[r'upload'] = this.upload;
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -46,6 +52,7 @@ class SystemConfigBackupsDto {
|
||||
|
||||
return SystemConfigBackupsDto(
|
||||
database: DatabaseBackupConfig.fromJson(json[r'database'])!,
|
||||
upload: UploadBackupConfig.fromJson(json[r'upload'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -94,6 +101,7 @@ class SystemConfigBackupsDto {
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'database',
|
||||
'upload',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ class SystemConfigNightlyTasksDto {
|
||||
required this.databaseCleanup,
|
||||
required this.generateMemories,
|
||||
required this.missingThumbnails,
|
||||
required this.removeStaleUploads,
|
||||
required this.startTime,
|
||||
required this.syncQuotaUsage,
|
||||
});
|
||||
@@ -33,6 +34,8 @@ class SystemConfigNightlyTasksDto {
|
||||
/// Missing thumbnails
|
||||
bool missingThumbnails;
|
||||
|
||||
bool removeStaleUploads;
|
||||
|
||||
String startTime;
|
||||
|
||||
/// Sync quota usage
|
||||
@@ -44,6 +47,7 @@ class SystemConfigNightlyTasksDto {
|
||||
other.databaseCleanup == databaseCleanup &&
|
||||
other.generateMemories == generateMemories &&
|
||||
other.missingThumbnails == missingThumbnails &&
|
||||
other.removeStaleUploads == removeStaleUploads &&
|
||||
other.startTime == startTime &&
|
||||
other.syncQuotaUsage == syncQuotaUsage;
|
||||
|
||||
@@ -54,11 +58,12 @@ class SystemConfigNightlyTasksDto {
|
||||
(databaseCleanup.hashCode) +
|
||||
(generateMemories.hashCode) +
|
||||
(missingThumbnails.hashCode) +
|
||||
(removeStaleUploads.hashCode) +
|
||||
(startTime.hashCode) +
|
||||
(syncQuotaUsage.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SystemConfigNightlyTasksDto[clusterNewFaces=$clusterNewFaces, databaseCleanup=$databaseCleanup, generateMemories=$generateMemories, missingThumbnails=$missingThumbnails, startTime=$startTime, syncQuotaUsage=$syncQuotaUsage]';
|
||||
String toString() => 'SystemConfigNightlyTasksDto[clusterNewFaces=$clusterNewFaces, databaseCleanup=$databaseCleanup, generateMemories=$generateMemories, missingThumbnails=$missingThumbnails, removeStaleUploads=$removeStaleUploads, startTime=$startTime, syncQuotaUsage=$syncQuotaUsage]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -66,6 +71,7 @@ class SystemConfigNightlyTasksDto {
|
||||
json[r'databaseCleanup'] = this.databaseCleanup;
|
||||
json[r'generateMemories'] = this.generateMemories;
|
||||
json[r'missingThumbnails'] = this.missingThumbnails;
|
||||
json[r'removeStaleUploads'] = this.removeStaleUploads;
|
||||
json[r'startTime'] = this.startTime;
|
||||
json[r'syncQuotaUsage'] = this.syncQuotaUsage;
|
||||
return json;
|
||||
@@ -84,6 +90,7 @@ class SystemConfigNightlyTasksDto {
|
||||
databaseCleanup: mapValueOfType<bool>(json, r'databaseCleanup')!,
|
||||
generateMemories: mapValueOfType<bool>(json, r'generateMemories')!,
|
||||
missingThumbnails: mapValueOfType<bool>(json, r'missingThumbnails')!,
|
||||
removeStaleUploads: mapValueOfType<bool>(json, r'removeStaleUploads')!,
|
||||
startTime: mapValueOfType<String>(json, r'startTime')!,
|
||||
syncQuotaUsage: mapValueOfType<bool>(json, r'syncQuotaUsage')!,
|
||||
);
|
||||
@@ -137,6 +144,7 @@ class SystemConfigNightlyTasksDto {
|
||||
'databaseCleanup',
|
||||
'generateMemories',
|
||||
'missingThumbnails',
|
||||
'removeStaleUploads',
|
||||
'startTime',
|
||||
'syncQuotaUsage',
|
||||
};
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class UploadBackupConfig {
|
||||
/// Returns a new [UploadBackupConfig] instance.
|
||||
UploadBackupConfig({
|
||||
required this.maxAgeHours,
|
||||
});
|
||||
|
||||
/// Minimum value: 1
|
||||
num maxAgeHours;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is UploadBackupConfig &&
|
||||
other.maxAgeHours == maxAgeHours;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(maxAgeHours.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UploadBackupConfig[maxAgeHours=$maxAgeHours]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'maxAgeHours'] = this.maxAgeHours;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [UploadBackupConfig] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static UploadBackupConfig? fromJson(dynamic value) {
|
||||
upgradeDto(value, "UploadBackupConfig");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return UploadBackupConfig(
|
||||
maxAgeHours: num.parse('${json[r'maxAgeHours']}'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<UploadBackupConfig> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <UploadBackupConfig>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = UploadBackupConfig.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, UploadBackupConfig> mapFromJson(dynamic json) {
|
||||
final map = <String, UploadBackupConfig>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = UploadBackupConfig.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of UploadBackupConfig-objects as value to a dart map
|
||||
static Map<String, List<UploadBackupConfig>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<UploadBackupConfig>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = UploadBackupConfig.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'maxAgeHours',
|
||||
};
|
||||
}
|
||||
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class UploadOkDto {
|
||||
/// Returns a new [UploadOkDto] instance.
|
||||
UploadOkDto({
|
||||
required this.id,
|
||||
});
|
||||
|
||||
String id;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is UploadOkDto &&
|
||||
other.id == id;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(id.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UploadOkDto[id=$id]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'id'] = this.id;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [UploadOkDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static UploadOkDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "UploadOkDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return UploadOkDto(
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<UploadOkDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <UploadOkDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = UploadOkDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, UploadOkDto> mapFromJson(dynamic json) {
|
||||
final map = <String, UploadOkDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = UploadOkDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of UploadOkDto-objects as value to a dart map
|
||||
static Map<String, List<UploadOkDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<UploadOkDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = UploadOkDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'id',
|
||||
};
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: 'none'
|
||||
version: 2.6.2+3040
|
||||
version: 2.6.3+3041
|
||||
|
||||
environment:
|
||||
sdk: '>=3.8.0 <4.0.0'
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/cache_aware_listener_tracker.mixin.dart';
|
||||
|
||||
class TestImageCompleter extends ImageStreamCompleter with CacheAwareListenerTrackerMixin {
|
||||
bool wasCancelled = false;
|
||||
|
||||
TestImageCompleter({required bool hadInitialImage}) {
|
||||
setupListenerTracking(
|
||||
hadInitialImage: hadInitialImage,
|
||||
onLastListenerRemoved: () {
|
||||
wasCancelled = true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void setImage(ImageInfo image) {
|
||||
super.setImage(image);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
late ImageCache cache;
|
||||
late ImageStreamListener uiListener;
|
||||
|
||||
setUp(() {
|
||||
// Create a fresh, real Flutter ImageCache for every test
|
||||
cache = ImageCache();
|
||||
uiListener = ImageStreamListener((_, __) {});
|
||||
});
|
||||
|
||||
group('CacheAwareListenerTrackerMixin with Real ImageCache', () {
|
||||
|
||||
testWidgets('cancels fetch when UI detaches before completion', (WidgetTester tester) async {
|
||||
final completer = TestImageCompleter(hadInitialImage: false);
|
||||
final key = Object();
|
||||
|
||||
// 1. Request image from the real cache (simulating the provider)
|
||||
final stream = cache.putIfAbsent(key, () => completer)!;
|
||||
|
||||
// 2. UI attaches
|
||||
stream.addListener(uiListener);
|
||||
expect(completer.wasCancelled, isFalse);
|
||||
|
||||
// 3. Simulate asynchronous network delay...
|
||||
await tester.pump(const Duration(milliseconds: 150));
|
||||
|
||||
// 4. User scrolls away before network finishes. UI detaches.
|
||||
stream.removeListener(uiListener);
|
||||
|
||||
expect(completer.wasCancelled, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('survives cache eviction while UI listener is still attached', (WidgetTester tester) async {
|
||||
final completer = TestImageCompleter(hadInitialImage: false);
|
||||
final key = Object();
|
||||
|
||||
// 1. Request image and attach UI
|
||||
final stream = cache.putIfAbsent(key, () => completer)!;
|
||||
stream.addListener(uiListener);
|
||||
|
||||
// 2. Simulate app going to background -> OS Memory Warning -> Cache clears
|
||||
cache.clear();
|
||||
|
||||
// Even though the real cache just aggressively detached its listener,
|
||||
// the stream MUST survive because the UI widget is still on screen!
|
||||
expect(completer.wasCancelled, isFalse);
|
||||
|
||||
// 3. UI widget finally detaches
|
||||
stream.removeListener(uiListener);
|
||||
expect(completer.wasCancelled, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('survives synchronous cache detach during putIfAbsent with initialImage', (WidgetTester tester) async {
|
||||
final completer = TestImageCompleter(hadInitialImage: true);
|
||||
final key = Object();
|
||||
|
||||
// Run image creation outside FakeAsync zone to avoid hang
|
||||
late ui.Image dummyImage;
|
||||
await tester.runAsync(() async {
|
||||
dummyImage = await createTestImage(width: 1, height: 1);
|
||||
});
|
||||
|
||||
final initialImageInfo = ImageInfo(image: dummyImage);
|
||||
|
||||
final stream = cache.putIfAbsent(key, () {
|
||||
completer.setImage(initialImageInfo);
|
||||
return completer;
|
||||
})!;
|
||||
|
||||
expect(completer.wasCancelled, isFalse);
|
||||
|
||||
stream.addListener(uiListener);
|
||||
expect(completer.wasCancelled, isFalse);
|
||||
|
||||
stream.removeListener(uiListener);
|
||||
expect(completer.wasCancelled, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('fires cleanup on full abandonment even after successful fetch', (WidgetTester tester) async {
|
||||
final completer = TestImageCompleter(hadInitialImage: false);
|
||||
final key = Object();
|
||||
|
||||
final stream = cache.putIfAbsent(key, () => completer)!;
|
||||
stream.addListener(uiListener);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
|
||||
// Run image creation outside FakeAsync zone to avoid hang
|
||||
late ui.Image dummyImage;
|
||||
await tester.runAsync(() async {
|
||||
dummyImage = await createTestImage(width: 1, height: 1);
|
||||
});
|
||||
|
||||
completer.setImage(ImageInfo(image: dummyImage));
|
||||
|
||||
stream.removeListener(uiListener);
|
||||
|
||||
// The stream is completely abandoned (0 listeners), so it fires the cleanup hook.
|
||||
// Since the image is already downloaded, canceling the network token is a safe no-op.
|
||||
expect(completer.wasCancelled, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('Multiple UI listeners — only all detached, should cancel', (WidgetTester tester) async {
|
||||
final completer = TestImageCompleter(hadInitialImage: false);
|
||||
final key = Object();
|
||||
|
||||
final stream = cache.putIfAbsent(key, () => completer)!;
|
||||
|
||||
final uiListener2 = ImageStreamListener((_, __) {});
|
||||
stream.addListener(uiListener);
|
||||
stream.addListener(uiListener2);
|
||||
|
||||
// First UI detach leaves cache + one UI → no cancel
|
||||
stream.removeListener(uiListener);
|
||||
expect(completer.wasCancelled, isFalse);
|
||||
|
||||
// Second UI detach leaves only cache → cancel
|
||||
stream.removeListener(uiListener2);
|
||||
expect(completer.wasCancelled, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('Listener misidentification: new listener after cache eviction is not treated as cache', (WidgetTester tester) async {
|
||||
final completer = TestImageCompleter(hadInitialImage: false);
|
||||
final key = Object();
|
||||
|
||||
final stream = cache.putIfAbsent(key, () => completer)!;
|
||||
|
||||
// UI attaches
|
||||
stream.addListener(uiListener);
|
||||
|
||||
// Cache eviction removes the cache listener
|
||||
cache.clear();
|
||||
expect(completer.wasCancelled, isFalse);
|
||||
|
||||
// A second UI listener attaches — must NOT be treated as cache
|
||||
final uiListener2 = ImageStreamListener((_, __) {});
|
||||
stream.addListener(uiListener2);
|
||||
|
||||
// Remove first UI listener; second UI still active → no cancel
|
||||
stream.removeListener(uiListener);
|
||||
expect(completer.wasCancelled, isFalse);
|
||||
|
||||
// Remove second UI listener; completely abandoned → cancel
|
||||
stream.removeListener(uiListener2);
|
||||
expect(completer.wasCancelled, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('No UI listener ever attaches (cache-only) — cache detaches should cancel', (WidgetTester tester) async {
|
||||
final completer = TestImageCompleter(hadInitialImage: false);
|
||||
final key = Object();
|
||||
|
||||
cache.putIfAbsent(key, () => completer);
|
||||
|
||||
// Cache eviction removes the only listener
|
||||
cache.clear();
|
||||
expect(completer.wasCancelled, isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -5285,6 +5285,65 @@
|
||||
"x-immich-state": "Stable"
|
||||
}
|
||||
},
|
||||
"/duplicates/resolve": {
|
||||
"post": {
|
||||
"description": "Resolve duplicate groups by synchronizing metadata across assets and deleting/trashing duplicates.",
|
||||
"operationId": "resolveDuplicates",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DuplicateResolveDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/BulkIdResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Resolve duplicate groups",
|
||||
"tags": [
|
||||
"Duplicates"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v3.0.0",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v3.0.0",
|
||||
"state": "Alpha"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "duplicate.delete",
|
||||
"x-immich-state": "Alpha"
|
||||
}
|
||||
},
|
||||
"/duplicates/{id}": {
|
||||
"delete": {
|
||||
"description": "Delete a single duplicate asset specified by its ID.",
|
||||
@@ -13987,6 +14046,320 @@
|
||||
"x-immich-state": "Stable"
|
||||
}
|
||||
},
|
||||
"/upload": {
|
||||
"options": {
|
||||
"operationId": "getUploadOptions",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Upload"
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"operationId": "startUpload",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "content-length",
|
||||
"in": "header",
|
||||
"description": "Non-negative size of the request body in bytes.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "key",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "repr-digest",
|
||||
"in": "header",
|
||||
"description": "RFC 9651 structured dictionary containing an `sha` (bytesequence) checksum used to detect duplicate files and validate data integrity.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "slug",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "upload-complete",
|
||||
"in": "header",
|
||||
"description": "Structured boolean indicating whether this request completes the file. Use Upload-Incomplete instead for version <= 3.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "upload-draft-interop-version",
|
||||
"in": "header",
|
||||
"description": "Indicates the version of the RUFH protocol supported by the client.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "x-immich-asset-data",
|
||||
"in": "header",
|
||||
"description": "RFC 9651 structured dictionary containing asset metadata with the following keys:\n- device-asset-id (string, required): Unique device asset identifier\n- device-id (string, required): Device identifier\n- file-created-at (string/date, required): ISO 8601 date string or Unix timestamp\n- file-modified-at (string/date, required): ISO 8601 date string or Unix timestamp\n- filename (string, required): Original filename\n- is-favorite (boolean, optional): Favorite status\n- live-photo-video-id (string, optional): Live photo ID for assets from iOS devices\n- icloud-id (string, optional): iCloud identifier for assets from iOS devices",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UploadOkDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
},
|
||||
"201": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Upload"
|
||||
],
|
||||
"x-immich-permission": "asset.upload"
|
||||
}
|
||||
},
|
||||
"/upload/{id}": {
|
||||
"delete": {
|
||||
"operationId": "cancelUpload",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "key",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "slug",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Upload"
|
||||
],
|
||||
"x-immich-permission": "asset.upload"
|
||||
},
|
||||
"head": {
|
||||
"operationId": "getUploadStatus",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "key",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "slug",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "upload-draft-interop-version",
|
||||
"in": "header",
|
||||
"description": "Indicates the version of the RUFH protocol supported by the client.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Upload"
|
||||
],
|
||||
"x-immich-permission": "asset.upload"
|
||||
},
|
||||
"patch": {
|
||||
"operationId": "resumeUpload",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "content-length",
|
||||
"in": "header",
|
||||
"description": "Non-negative size of the request body in bytes.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "key",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "slug",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "upload-complete",
|
||||
"in": "header",
|
||||
"description": "Structured boolean indicating whether this request completes the file. Use Upload-Incomplete instead for version <= 3.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "upload-draft-interop-version",
|
||||
"in": "header",
|
||||
"description": "Indicates the version of the RUFH protocol supported by the client.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "upload-offset",
|
||||
"in": "header",
|
||||
"description": "Non-negative byte offset indicating the starting position of the data in the request body within the entire file.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UploadOkDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Upload"
|
||||
],
|
||||
"x-immich-permission": "asset.upload"
|
||||
}
|
||||
},
|
||||
"/users": {
|
||||
"get": {
|
||||
"description": "Retrieve a list of all users on the server.",
|
||||
@@ -15166,7 +15539,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "2.6.2",
|
||||
"version": "2.6.3",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [
|
||||
@@ -17299,7 +17672,8 @@
|
||||
"duplicate",
|
||||
"no_permission",
|
||||
"not_found",
|
||||
"unknown"
|
||||
"unknown",
|
||||
"validation"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@@ -17311,10 +17685,14 @@
|
||||
"duplicate",
|
||||
"no_permission",
|
||||
"not_found",
|
||||
"unknown"
|
||||
"unknown",
|
||||
"validation"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"errorMessage": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "ID",
|
||||
"type": "string"
|
||||
@@ -17828,6 +18206,52 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"DuplicateResolveDto": {
|
||||
"properties": {
|
||||
"groups": {
|
||||
"description": "List of duplicate groups to resolve",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/DuplicateResolveGroupDto"
|
||||
},
|
||||
"minItems": 1,
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"groups"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"DuplicateResolveGroupDto": {
|
||||
"properties": {
|
||||
"duplicateId": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"keepAssetIds": {
|
||||
"description": "Asset IDs to keep",
|
||||
"items": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"trashAssetIds": {
|
||||
"description": "Asset IDs to trash or delete",
|
||||
"items": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"duplicateId",
|
||||
"keepAssetIds",
|
||||
"trashAssetIds"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"DuplicateResponseDto": {
|
||||
"properties": {
|
||||
"assets": {
|
||||
@@ -17840,11 +18264,20 @@
|
||||
"duplicateId": {
|
||||
"description": "Duplicate group ID",
|
||||
"type": "string"
|
||||
},
|
||||
"suggestedKeepAssetIds": {
|
||||
"description": "Suggested asset IDs to keep based on file size and EXIF data",
|
||||
"items": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"assets",
|
||||
"duplicateId"
|
||||
"duplicateId",
|
||||
"suggestedKeepAssetIds"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@@ -18153,6 +18586,8 @@
|
||||
"AssetFileMigration",
|
||||
"AssetGenerateThumbnailsQueueAll",
|
||||
"AssetGenerateThumbnails",
|
||||
"PartialAssetCleanup",
|
||||
"PartialAssetCleanupQueueAll",
|
||||
"AuditLogCleanup",
|
||||
"AuditTableCleanup",
|
||||
"DatabaseBackup",
|
||||
@@ -19129,7 +19564,7 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"withPeople": {
|
||||
"description": "Include assets with people",
|
||||
"description": "Include people data in response",
|
||||
"type": "boolean"
|
||||
},
|
||||
"withStacked": {
|
||||
@@ -20868,7 +21303,7 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"withPeople": {
|
||||
"description": "Include assets with people",
|
||||
"description": "Include people data in response",
|
||||
"type": "boolean"
|
||||
},
|
||||
"withStacked": {
|
||||
@@ -23812,10 +24247,14 @@
|
||||
"properties": {
|
||||
"database": {
|
||||
"$ref": "#/components/schemas/DatabaseBackupConfig"
|
||||
},
|
||||
"upload": {
|
||||
"$ref": "#/components/schemas/UploadBackupConfig"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"database"
|
||||
"database",
|
||||
"upload"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@@ -24405,6 +24844,9 @@
|
||||
"description": "Missing thumbnails",
|
||||
"type": "boolean"
|
||||
},
|
||||
"removeStaleUploads": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"startTime": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -24418,6 +24860,7 @@
|
||||
"databaseCleanup",
|
||||
"generateMemories",
|
||||
"missingThumbnails",
|
||||
"removeStaleUploads",
|
||||
"startTime",
|
||||
"syncQuotaUsage"
|
||||
],
|
||||
@@ -25377,6 +25820,29 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"UploadBackupConfig": {
|
||||
"properties": {
|
||||
"maxAgeHours": {
|
||||
"minimum": 1,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"maxAgeHours"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"UploadOkDto": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"UsageByUserDto": {
|
||||
"properties": {
|
||||
"photos": {
|
||||
|
||||
@@ -1 +1 @@
|
||||
24.13.1
|
||||
24.14.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "2.6.2",
|
||||
"version": "2.6.3",
|
||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||
"type": "module",
|
||||
"main": "./build/index.js",
|
||||
@@ -19,7 +19,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.11.0",
|
||||
"@types/node": "^24.12.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"repository": {
|
||||
@@ -28,6 +28,6 @@
|
||||
"directory": "open-api/typescript-sdk"
|
||||
},
|
||||
"volta": {
|
||||
"node": "24.13.1"
|
||||
"node": "24.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Immich
|
||||
* 2.6.2
|
||||
* 2.6.3
|
||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||
* See https://www.npmjs.com/package/oazapfts
|
||||
*/
|
||||
@@ -725,6 +725,7 @@ export type BulkIdsDto = {
|
||||
export type BulkIdResponseDto = {
|
||||
/** Error reason if failed */
|
||||
error?: Error;
|
||||
errorMessage?: string;
|
||||
/** ID */
|
||||
id: string;
|
||||
/** Whether operation succeeded */
|
||||
@@ -1163,6 +1164,19 @@ export type DuplicateResponseDto = {
|
||||
assets: AssetResponseDto[];
|
||||
/** Duplicate group ID */
|
||||
duplicateId: string;
|
||||
/** Suggested asset IDs to keep based on file size and EXIF data */
|
||||
suggestedKeepAssetIds: string[];
|
||||
};
|
||||
export type DuplicateResolveGroupDto = {
|
||||
duplicateId: string;
|
||||
/** Asset IDs to keep */
|
||||
keepAssetIds: string[];
|
||||
/** Asset IDs to trash or delete */
|
||||
trashAssetIds: string[];
|
||||
};
|
||||
export type DuplicateResolveDto = {
|
||||
/** List of duplicate groups to resolve */
|
||||
groups: DuplicateResolveGroupDto[];
|
||||
};
|
||||
export type PersonResponseDto = {
|
||||
/** Person date of birth */
|
||||
@@ -1741,7 +1755,7 @@ export type MetadataSearchDto = {
|
||||
withDeleted?: boolean;
|
||||
/** Include EXIF data in response */
|
||||
withExif?: boolean;
|
||||
/** Include assets with people */
|
||||
/** Include people data in response */
|
||||
withPeople?: boolean;
|
||||
/** Include stacked assets */
|
||||
withStacked?: boolean;
|
||||
@@ -1855,7 +1869,7 @@ export type RandomSearchDto = {
|
||||
withDeleted?: boolean;
|
||||
/** Include EXIF data in response */
|
||||
withExif?: boolean;
|
||||
/** Include assets with people */
|
||||
/** Include people data in response */
|
||||
withPeople?: boolean;
|
||||
/** Include stacked assets */
|
||||
withStacked?: boolean;
|
||||
@@ -2393,8 +2407,12 @@ export type DatabaseBackupConfig = {
|
||||
/** Keep last amount */
|
||||
keepLastAmount: number;
|
||||
};
|
||||
export type UploadBackupConfig = {
|
||||
maxAgeHours: number;
|
||||
};
|
||||
export type SystemConfigBackupsDto = {
|
||||
database: DatabaseBackupConfig;
|
||||
upload: UploadBackupConfig;
|
||||
};
|
||||
export type SystemConfigFFmpegDto = {
|
||||
/** Transcode hardware acceleration */
|
||||
@@ -2584,6 +2602,7 @@ export type SystemConfigNightlyTasksDto = {
|
||||
generateMemories: boolean;
|
||||
/** Missing thumbnails */
|
||||
missingThumbnails: boolean;
|
||||
removeStaleUploads: boolean;
|
||||
startTime: string;
|
||||
/** Sync quota usage */
|
||||
syncQuotaUsage: boolean;
|
||||
@@ -2799,6 +2818,9 @@ export type TrashResponseDto = {
|
||||
/** Number of items in trash */
|
||||
count: number;
|
||||
};
|
||||
export type UploadOkDto = {
|
||||
id: string;
|
||||
};
|
||||
export type UserUpdateMeDto = {
|
||||
/** Avatar color */
|
||||
avatarColor?: (UserAvatarColor) | null;
|
||||
@@ -4531,6 +4553,21 @@ export function getAssetDuplicates(opts?: Oazapfts.RequestOpts) {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Resolve duplicate groups
|
||||
*/
|
||||
export function resolveDuplicates({ duplicateResolveDto }: {
|
||||
duplicateResolveDto: DuplicateResolveDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: BulkIdResponseDto[];
|
||||
}>("/duplicates/resolve", oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: duplicateResolveDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Delete a duplicate
|
||||
*/
|
||||
@@ -6536,6 +6573,97 @@ export function restoreAssets({ bulkIdsDto }: {
|
||||
body: bulkIdsDto
|
||||
})));
|
||||
}
|
||||
export function getUploadOptions(opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText("/upload", {
|
||||
...opts,
|
||||
method: "OPTIONS"
|
||||
}));
|
||||
}
|
||||
export function startUpload({ contentLength, key, reprDigest, slug, uploadComplete, uploadDraftInteropVersion, xImmichAssetData }: {
|
||||
contentLength: string;
|
||||
key?: string;
|
||||
reprDigest: string;
|
||||
slug?: string;
|
||||
uploadComplete?: string;
|
||||
uploadDraftInteropVersion?: string;
|
||||
xImmichAssetData: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: UploadOkDto;
|
||||
} | {
|
||||
status: 201;
|
||||
}>(`/upload${QS.query(QS.explode({
|
||||
key,
|
||||
slug
|
||||
}))}`, {
|
||||
...opts,
|
||||
method: "POST",
|
||||
headers: oazapfts.mergeHeaders(opts?.headers, {
|
||||
"content-length": contentLength,
|
||||
"repr-digest": reprDigest,
|
||||
"upload-complete": uploadComplete,
|
||||
"upload-draft-interop-version": uploadDraftInteropVersion,
|
||||
"x-immich-asset-data": xImmichAssetData
|
||||
})
|
||||
}));
|
||||
}
|
||||
export function cancelUpload({ id, key, slug }: {
|
||||
id: string;
|
||||
key?: string;
|
||||
slug?: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText(`/upload/${encodeURIComponent(id)}${QS.query(QS.explode({
|
||||
key,
|
||||
slug
|
||||
}))}`, {
|
||||
...opts,
|
||||
method: "DELETE"
|
||||
}));
|
||||
}
|
||||
export function getUploadStatus({ id, key, slug, uploadDraftInteropVersion }: {
|
||||
id: string;
|
||||
key?: string;
|
||||
slug?: string;
|
||||
uploadDraftInteropVersion: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchText(`/upload/${encodeURIComponent(id)}${QS.query(QS.explode({
|
||||
key,
|
||||
slug
|
||||
}))}`, {
|
||||
...opts,
|
||||
method: "HEAD",
|
||||
headers: oazapfts.mergeHeaders(opts?.headers, {
|
||||
"upload-draft-interop-version": uploadDraftInteropVersion
|
||||
})
|
||||
}));
|
||||
}
|
||||
export function resumeUpload({ contentLength, id, key, slug, uploadComplete, uploadDraftInteropVersion, uploadOffset }: {
|
||||
contentLength: string;
|
||||
id: string;
|
||||
key?: string;
|
||||
slug?: string;
|
||||
uploadComplete: string;
|
||||
uploadDraftInteropVersion: string;
|
||||
uploadOffset: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: UploadOkDto;
|
||||
}>(`/upload/${encodeURIComponent(id)}${QS.query(QS.explode({
|
||||
key,
|
||||
slug
|
||||
}))}`, {
|
||||
...opts,
|
||||
method: "PATCH",
|
||||
headers: oazapfts.mergeHeaders(opts?.headers, {
|
||||
"content-length": contentLength,
|
||||
"upload-complete": uploadComplete,
|
||||
"upload-draft-interop-version": uploadDraftInteropVersion,
|
||||
"upload-offset": uploadOffset
|
||||
})
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Get all users
|
||||
*/
|
||||
@@ -6893,13 +7021,15 @@ export enum BulkIdErrorReason {
|
||||
Duplicate = "duplicate",
|
||||
NoPermission = "no_permission",
|
||||
NotFound = "not_found",
|
||||
Unknown = "unknown"
|
||||
Unknown = "unknown",
|
||||
Validation = "validation"
|
||||
}
|
||||
export enum Error {
|
||||
Duplicate = "duplicate",
|
||||
NoPermission = "no_permission",
|
||||
NotFound = "not_found",
|
||||
Unknown = "unknown"
|
||||
Unknown = "unknown",
|
||||
Validation = "validation"
|
||||
}
|
||||
export enum Permission {
|
||||
All = "all",
|
||||
@@ -7173,6 +7303,8 @@ export enum JobName {
|
||||
AssetFileMigration = "AssetFileMigration",
|
||||
AssetGenerateThumbnailsQueueAll = "AssetGenerateThumbnailsQueueAll",
|
||||
AssetGenerateThumbnails = "AssetGenerateThumbnails",
|
||||
PartialAssetCleanup = "PartialAssetCleanup",
|
||||
PartialAssetCleanupQueueAll = "PartialAssetCleanupQueueAll",
|
||||
AuditLogCleanup = "AuditLogCleanup",
|
||||
AuditTableCleanup = "AuditTableCleanup",
|
||||
DatabaseBackup = "DatabaseBackup",
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "immich-monorepo",
|
||||
"version": "2.6.2",
|
||||
"version": "2.6.3",
|
||||
"description": "Monorepo for Immich",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.30.3+sha512.c961d1e0a2d8e354ecaa5166b822516668b7f44cb5bd95122d590dd81922f606f5473b6d23ec4a5be05e7fcd18e8488d47d978bbe981872f1145d06e9a740017",
|
||||
"packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be",
|
||||
"engines": {
|
||||
"pnpm": ">=10.0.0"
|
||||
}
|
||||
|
||||
Generated
+107
-107
@@ -15,9 +15,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
|
||||
"integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -32,9 +32,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
|
||||
"integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -49,9 +49,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -66,9 +66,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -83,9 +83,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -100,9 +100,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -117,9 +117,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -134,9 +134,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -151,9 +151,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
|
||||
"integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -168,9 +168,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -185,9 +185,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
|
||||
"integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -202,9 +202,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
|
||||
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
|
||||
"integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -219,9 +219,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
|
||||
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
|
||||
"integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -236,9 +236,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
|
||||
"integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -253,9 +253,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
|
||||
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
|
||||
"integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -270,9 +270,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
|
||||
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
|
||||
"integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -287,9 +287,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -304,9 +304,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -321,9 +321,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -338,9 +338,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -355,9 +355,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -372,9 +372,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -389,9 +389,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -406,9 +406,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
|
||||
"integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -423,9 +423,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
|
||||
"integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -440,9 +440,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
|
||||
"integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -467,9 +467,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
||||
"version": "0.27.4",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
|
||||
"integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -480,32 +480,32 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.3",
|
||||
"@esbuild/android-arm": "0.27.3",
|
||||
"@esbuild/android-arm64": "0.27.3",
|
||||
"@esbuild/android-x64": "0.27.3",
|
||||
"@esbuild/darwin-arm64": "0.27.3",
|
||||
"@esbuild/darwin-x64": "0.27.3",
|
||||
"@esbuild/freebsd-arm64": "0.27.3",
|
||||
"@esbuild/freebsd-x64": "0.27.3",
|
||||
"@esbuild/linux-arm": "0.27.3",
|
||||
"@esbuild/linux-arm64": "0.27.3",
|
||||
"@esbuild/linux-ia32": "0.27.3",
|
||||
"@esbuild/linux-loong64": "0.27.3",
|
||||
"@esbuild/linux-mips64el": "0.27.3",
|
||||
"@esbuild/linux-ppc64": "0.27.3",
|
||||
"@esbuild/linux-riscv64": "0.27.3",
|
||||
"@esbuild/linux-s390x": "0.27.3",
|
||||
"@esbuild/linux-x64": "0.27.3",
|
||||
"@esbuild/netbsd-arm64": "0.27.3",
|
||||
"@esbuild/netbsd-x64": "0.27.3",
|
||||
"@esbuild/openbsd-arm64": "0.27.3",
|
||||
"@esbuild/openbsd-x64": "0.27.3",
|
||||
"@esbuild/openharmony-arm64": "0.27.3",
|
||||
"@esbuild/sunos-x64": "0.27.3",
|
||||
"@esbuild/win32-arm64": "0.27.3",
|
||||
"@esbuild/win32-ia32": "0.27.3",
|
||||
"@esbuild/win32-x64": "0.27.3"
|
||||
"@esbuild/aix-ppc64": "0.27.4",
|
||||
"@esbuild/android-arm": "0.27.4",
|
||||
"@esbuild/android-arm64": "0.27.4",
|
||||
"@esbuild/android-x64": "0.27.4",
|
||||
"@esbuild/darwin-arm64": "0.27.4",
|
||||
"@esbuild/darwin-x64": "0.27.4",
|
||||
"@esbuild/freebsd-arm64": "0.27.4",
|
||||
"@esbuild/freebsd-x64": "0.27.4",
|
||||
"@esbuild/linux-arm": "0.27.4",
|
||||
"@esbuild/linux-arm64": "0.27.4",
|
||||
"@esbuild/linux-ia32": "0.27.4",
|
||||
"@esbuild/linux-loong64": "0.27.4",
|
||||
"@esbuild/linux-mips64el": "0.27.4",
|
||||
"@esbuild/linux-ppc64": "0.27.4",
|
||||
"@esbuild/linux-riscv64": "0.27.4",
|
||||
"@esbuild/linux-s390x": "0.27.4",
|
||||
"@esbuild/linux-x64": "0.27.4",
|
||||
"@esbuild/netbsd-arm64": "0.27.4",
|
||||
"@esbuild/netbsd-x64": "0.27.4",
|
||||
"@esbuild/openbsd-arm64": "0.27.4",
|
||||
"@esbuild/openbsd-x64": "0.27.4",
|
||||
"@esbuild/openharmony-arm64": "0.27.4",
|
||||
"@esbuild/sunos-x64": "0.27.4",
|
||||
"@esbuild/win32-arm64": "0.27.4",
|
||||
"@esbuild/win32-ia32": "0.27.4",
|
||||
"@esbuild/win32-x64": "0.27.4"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
|
||||
Generated
+1076
-1190
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,10 @@
|
||||
"matchUpdateTypes": ["major"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["ghcr.io/immich-app/base-server-*"],
|
||||
"maxMajorIncrement": 0
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["ruby"],
|
||||
"groupName": "ruby",
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
24.13.1
|
||||
24.14.0
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user