mirror of
https://github.com/immich-app/immich.git
synced 2025-12-17 01:11:13 +03:00
feat: upload backups
This commit is contained in:
1
mobile/openapi/README.md
generated
1
mobile/openapi/README.md
generated
@@ -165,6 +165,7 @@ Class | Method | HTTP request | Description
|
|||||||
*MaintenanceAdminApi* | [**maintenanceStatus**](doc//MaintenanceAdminApi.md#maintenancestatus) | **GET** /admin/maintenance/status | Get maintenance mode status
|
*MaintenanceAdminApi* | [**maintenanceStatus**](doc//MaintenanceAdminApi.md#maintenancestatus) | **GET** /admin/maintenance/status | Get maintenance mode status
|
||||||
*MaintenanceAdminApi* | [**setMaintenanceMode**](doc//MaintenanceAdminApi.md#setmaintenancemode) | **POST** /admin/maintenance | Set maintenance mode
|
*MaintenanceAdminApi* | [**setMaintenanceMode**](doc//MaintenanceAdminApi.md#setmaintenancemode) | **POST** /admin/maintenance | Set maintenance mode
|
||||||
*MaintenanceAdminApi* | [**startRestoreFlow**](doc//MaintenanceAdminApi.md#startrestoreflow) | **POST** /admin/maintenance/backups/restore | Start backup restore flow
|
*MaintenanceAdminApi* | [**startRestoreFlow**](doc//MaintenanceAdminApi.md#startrestoreflow) | **POST** /admin/maintenance/backups/restore | Start backup restore flow
|
||||||
|
*MaintenanceAdminApi* | [**uploadBackup**](doc//MaintenanceAdminApi.md#uploadbackup) | **POST** /admin/maintenance/backups/upload | Upload asset
|
||||||
*MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers | Retrieve map markers
|
*MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers | Retrieve map markers
|
||||||
*MapApi* | [**reverseGeocode**](doc//MapApi.md#reversegeocode) | **GET** /map/reverse-geocode | Reverse geocode coordinates
|
*MapApi* | [**reverseGeocode**](doc//MapApi.md#reversegeocode) | **GET** /map/reverse-geocode | Reverse geocode coordinates
|
||||||
*MemoriesApi* | [**addMemoryAssets**](doc//MemoriesApi.md#addmemoryassets) | **PUT** /memories/{id}/assets | Add assets to a memory
|
*MemoriesApi* | [**addMemoryAssets**](doc//MemoriesApi.md#addmemoryassets) | **PUT** /memories/{id}/assets | Add assets to a memory
|
||||||
|
|||||||
58
mobile/openapi/lib/api/maintenance_admin_api.dart
generated
58
mobile/openapi/lib/api/maintenance_admin_api.dart
generated
@@ -304,4 +304,62 @@ class MaintenanceAdminApi {
|
|||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Upload asset
|
||||||
|
///
|
||||||
|
/// Uploads a new asset to the server.
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [MultipartFile] file:
|
||||||
|
Future<Response> uploadBackupWithHttpInfo({ MultipartFile? file, }) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/admin/maintenance/backups/upload';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['multipart/form-data'];
|
||||||
|
|
||||||
|
bool hasFields = false;
|
||||||
|
final mp = MultipartRequest('POST', Uri.parse(apiPath));
|
||||||
|
if (file != null) {
|
||||||
|
hasFields = true;
|
||||||
|
mp.fields[r'file'] = file.field;
|
||||||
|
mp.files.add(file);
|
||||||
|
}
|
||||||
|
if (hasFields) {
|
||||||
|
postBody = mp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'POST',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload asset
|
||||||
|
///
|
||||||
|
/// Uploads a new asset to the server.
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [MultipartFile] file:
|
||||||
|
Future<void> uploadBackup({ MultipartFile? file, }) async {
|
||||||
|
final response = await uploadBackupWithHttpInfo( file: file, );
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -446,6 +446,57 @@
|
|||||||
"x-immich-state": "Alpha"
|
"x-immich-state": "Alpha"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/admin/maintenance/backups/upload": {
|
||||||
|
"post": {
|
||||||
|
"description": "Uploads a new asset to the server.",
|
||||||
|
"operationId": "uploadBackup",
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"multipart/form-data": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/MaintenanceUploadBackupDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Backup Upload",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": "Upload asset",
|
||||||
|
"tags": [
|
||||||
|
"Maintenance (admin)"
|
||||||
|
],
|
||||||
|
"x-immich-admin-only": true,
|
||||||
|
"x-immich-history": [
|
||||||
|
{
|
||||||
|
"version": "v9.9.9",
|
||||||
|
"state": "Added"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v9.9.9",
|
||||||
|
"state": "Alpha"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-immich-permission": "maintenance",
|
||||||
|
"x-immich-state": "Alpha"
|
||||||
|
}
|
||||||
|
},
|
||||||
"/admin/maintenance/backups/{filename}": {
|
"/admin/maintenance/backups/{filename}": {
|
||||||
"delete": {
|
"delete": {
|
||||||
"description": "Delete a backup by its filename",
|
"description": "Delete a backup by its filename",
|
||||||
@@ -16753,6 +16804,15 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"MaintenanceUploadBackupDto": {
|
||||||
|
"properties": {
|
||||||
|
"file": {
|
||||||
|
"format": "binary",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"ManualJobName": {
|
"ManualJobName": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"person-cleanup",
|
"person-cleanup",
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ export type MaintenanceListBackupsResponseDto = {
|
|||||||
backups: string[];
|
backups: string[];
|
||||||
failedBackups: string[];
|
failedBackups: string[];
|
||||||
};
|
};
|
||||||
|
export type MaintenanceUploadBackupDto = {
|
||||||
|
file?: Blob;
|
||||||
|
};
|
||||||
export type MaintenanceLoginDto = {
|
export type MaintenanceLoginDto = {
|
||||||
token?: string;
|
token?: string;
|
||||||
};
|
};
|
||||||
@@ -1875,6 +1878,18 @@ export function startRestoreFlow(opts?: Oazapfts.RequestOpts) {
|
|||||||
method: "POST"
|
method: "POST"
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Upload asset
|
||||||
|
*/
|
||||||
|
export function uploadBackup({ maintenanceUploadBackupDto }: {
|
||||||
|
maintenanceUploadBackupDto: MaintenanceUploadBackupDto;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchText("/admin/maintenance/backups/upload", oazapfts.multipart({
|
||||||
|
...opts,
|
||||||
|
method: "POST",
|
||||||
|
body: maintenanceUploadBackupDto
|
||||||
|
})));
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Delete backup
|
* Delete backup
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
import { BadRequestException, Body, Controller, Delete, Get, Param, Post, Res } from '@nestjs/common';
|
import {
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
BadRequestException,
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
FileTypeValidator,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
ParseFilePipe,
|
||||||
|
Post,
|
||||||
|
Res,
|
||||||
|
UploadedFile,
|
||||||
|
UseInterceptors,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
|
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
@@ -8,6 +22,7 @@ import {
|
|||||||
MaintenanceListBackupsResponseDto,
|
MaintenanceListBackupsResponseDto,
|
||||||
MaintenanceLoginDto,
|
MaintenanceLoginDto,
|
||||||
MaintenanceStatusResponseDto,
|
MaintenanceStatusResponseDto,
|
||||||
|
MaintenanceUploadBackupDto,
|
||||||
SetMaintenanceModeDto,
|
SetMaintenanceModeDto,
|
||||||
} from 'src/dtos/maintenance.dto';
|
} from 'src/dtos/maintenance.dto';
|
||||||
import { ApiTag, ImmichCookie, MaintenanceAction, Permission } from 'src/enum';
|
import { ApiTag, ImmichCookie, MaintenanceAction, Permission } from 'src/enum';
|
||||||
@@ -104,4 +119,21 @@ export class MaintenanceController {
|
|||||||
values: [{ key: ImmichCookie.MaintenanceToken, value: jwt }],
|
values: [{ key: ImmichCookie.MaintenanceToken, value: jwt }],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('backups/upload')
|
||||||
|
@Authenticated({ permission: Permission.Maintenance, admin: true })
|
||||||
|
@ApiConsumes('multipart/form-data')
|
||||||
|
@ApiBody({ description: 'Backup Upload', type: MaintenanceUploadBackupDto })
|
||||||
|
@Endpoint({
|
||||||
|
summary: 'Upload asset',
|
||||||
|
description: 'Uploads a new asset to the server.',
|
||||||
|
history: new HistoryBuilder().added('v9.9.9').alpha('v9.9.9'),
|
||||||
|
})
|
||||||
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
|
uploadBackup(
|
||||||
|
@UploadedFile(new ParseFilePipe({ validators: [new FileTypeValidator({ fileType: 'application/gzip' })] }))
|
||||||
|
file: Express.Multer.File,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.service.uploadBackup(file);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { MaintenanceAction } from 'src/enum';
|
import { MaintenanceAction } from 'src/enum';
|
||||||
import { ValidateEnum, ValidateString } from 'src/validation';
|
import { ValidateEnum, ValidateString } from 'src/validation';
|
||||||
|
|
||||||
@@ -31,3 +32,8 @@ export class MaintenanceListBackupsResponseDto {
|
|||||||
backups!: string[];
|
backups!: string[];
|
||||||
failedBackups!: string[];
|
failedBackups!: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class MaintenanceUploadBackupDto {
|
||||||
|
@ApiProperty({ type: 'string', format: 'binary', required: false })
|
||||||
|
file?: any;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,18 @@
|
|||||||
import { Body, Controller, Delete, Get, Param, Post, Req, Res } from '@nestjs/common';
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
FileTypeValidator,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
ParseFilePipe,
|
||||||
|
Post,
|
||||||
|
Req,
|
||||||
|
Res,
|
||||||
|
UploadedFile,
|
||||||
|
UseInterceptors,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import {
|
import {
|
||||||
MaintenanceAuthDto,
|
MaintenanceAuthDto,
|
||||||
@@ -62,4 +76,14 @@ export class MaintenanceWorkerController {
|
|||||||
async deleteBackup(@Param() { filename }: FilenameParamDto): Promise<void> {
|
async deleteBackup(@Param() { filename }: FilenameParamDto): Promise<void> {
|
||||||
return this.service.deleteBackup(filename);
|
return this.service.deleteBackup(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('admin/maintenance/backups/upload')
|
||||||
|
@MaintenanceRoute()
|
||||||
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
|
uploadBackup(
|
||||||
|
@UploadedFile(new ParseFilePipe({ validators: [new FileTypeValidator({ fileType: 'application/gzip' })] }))
|
||||||
|
file: Express.Multer.File,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.service.uploadBackup(file);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { type ApiService as _ApiService } from 'src/services/api.service';
|
|||||||
import { type BaseService as _BaseService } from 'src/services/base.service';
|
import { type BaseService as _BaseService } from 'src/services/base.service';
|
||||||
import { type ServerService as _ServerService } from 'src/services/server.service';
|
import { type ServerService as _ServerService } from 'src/services/server.service';
|
||||||
import { MaintenanceModeState } from 'src/types';
|
import { MaintenanceModeState } from 'src/types';
|
||||||
import { deleteBackup, listBackups, restoreBackup } from 'src/utils/backups';
|
import { deleteBackup, listBackups, restoreBackup, uploadBackup } from 'src/utils/backups';
|
||||||
import { getConfig } from 'src/utils/config';
|
import { getConfig } from 'src/utils/config';
|
||||||
import { createMaintenanceLoginUrl } from 'src/utils/maintenance';
|
import { createMaintenanceLoginUrl } from 'src/utils/maintenance';
|
||||||
import { getExternalDomain } from 'src/utils/misc';
|
import { getExternalDomain } from 'src/utils/misc';
|
||||||
@@ -283,6 +283,10 @@ export class MaintenanceWorkerService {
|
|||||||
return deleteBackup(this.backupRepos, filename);
|
return deleteBackup(this.backupRepos, filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async uploadBackup(file: Express.Multer.File): Promise<void> {
|
||||||
|
return uploadBackup(file);
|
||||||
|
}
|
||||||
|
|
||||||
private get backupRepos() {
|
private get backupRepos() {
|
||||||
return {
|
return {
|
||||||
logger: this.logger,
|
logger: this.logger,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { MaintenanceAuthDto, SetMaintenanceModeDto } from 'src/dtos/maintenance.
|
|||||||
import { MaintenanceAction, SystemMetadataKey } from 'src/enum';
|
import { MaintenanceAction, SystemMetadataKey } from 'src/enum';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { MaintenanceModeState } from 'src/types';
|
import { MaintenanceModeState } from 'src/types';
|
||||||
import { deleteBackup, listBackups } from 'src/utils/backups';
|
import { deleteBackup, listBackups, uploadBackup } from 'src/utils/backups';
|
||||||
import { createMaintenanceLoginUrl, generateMaintenanceSecret, signMaintenanceJwt } from 'src/utils/maintenance';
|
import { createMaintenanceLoginUrl, generateMaintenanceSecret, signMaintenanceJwt } from 'src/utils/maintenance';
|
||||||
import { getExternalDomain } from 'src/utils/misc';
|
import { getExternalDomain } from 'src/utils/misc';
|
||||||
|
|
||||||
@@ -83,6 +83,10 @@ export class MaintenanceService extends BaseService {
|
|||||||
return deleteBackup(this.backupRepos, filename);
|
return deleteBackup(this.backupRepos, filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async uploadBackup(file: Express.Multer.File): Promise<void> {
|
||||||
|
return uploadBackup(file);
|
||||||
|
}
|
||||||
|
|
||||||
private get backupRepos() {
|
private get backupRepos() {
|
||||||
return {
|
return {
|
||||||
logger: this.logger,
|
logger: this.logger,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
import { stat, writeFile } from 'node:fs/promises';
|
||||||
import path, { join } from 'node:path';
|
import path, { join } from 'node:path';
|
||||||
import { PassThrough, Readable, Writable } from 'node:stream';
|
import { PassThrough, Readable, Writable } from 'node:stream';
|
||||||
import { pipeline } from 'node:stream/promises';
|
import { pipeline } from 'node:stream/promises';
|
||||||
@@ -269,6 +271,18 @@ export async function listBackups({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function uploadBackup(file: Express.Multer.File): Promise<void> {
|
||||||
|
const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups);
|
||||||
|
const path = join(backupsFolder, file.originalname);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await stat(path);
|
||||||
|
throw new BadRequestException('File already exists!');
|
||||||
|
} catch {
|
||||||
|
await writeFile(path, file.buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function createSqlProgressStreams(cb: (progress: number) => void) {
|
function createSqlProgressStreams(cb: (progress: number) => void) {
|
||||||
const STDIN_START_MARKER = new TextEncoder().encode('FROM stdin');
|
const STDIN_START_MARKER = new TextEncoder().encode('FROM stdin');
|
||||||
const STDIN_END_MARKER = new TextEncoder().encode(String.raw`\.`);
|
const STDIN_END_MARKER = new TextEncoder().encode(String.raw`\.`);
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { uploadRequest } from '$lib/utils';
|
||||||
|
import { openFilePicker } from '$lib/utils/file-uploader';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { deleteBackup, listBackups, MaintenanceAction, setMaintenanceMode } from '@immich/sdk';
|
import {
|
||||||
|
deleteBackup,
|
||||||
|
getBaseUrl,
|
||||||
|
listBackups,
|
||||||
|
MaintenanceAction,
|
||||||
|
setMaintenanceMode,
|
||||||
|
type MaintenanceUploadBackupDto,
|
||||||
|
} from '@immich/sdk';
|
||||||
import { Button, Card, CardBody, HStack, modalManager, Stack, Text } from '@immich/ui';
|
import { Button, Card, CardBody, HStack, modalManager, Stack, Text } from '@immich/ui';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
@@ -83,9 +92,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let uploadProgress = $state(-1);
|
||||||
|
|
||||||
|
async function upload() {
|
||||||
|
const [file] = await openFilePicker({ multiple: false });
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
await uploadRequest<MaintenanceUploadBackupDto>({
|
||||||
|
url: getBaseUrl() + '/admin/maintenance/backups/upload',
|
||||||
|
data: formData,
|
||||||
|
onUploadProgress(event) {
|
||||||
|
uploadProgress = event.loaded / event.total;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
uploadProgress = 1;
|
||||||
|
|
||||||
|
const { backups: newList } = await listBackups();
|
||||||
|
backups = mapBackups(newList);
|
||||||
|
|
||||||
|
uploadProgress = -1;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Stack gap={2} class="mt-4 text-left">
|
<Stack gap={2} class="mt-4 text-left">
|
||||||
|
<Card>
|
||||||
|
<CardBody>
|
||||||
|
{#if uploadProgress === -1}
|
||||||
|
<HStack>
|
||||||
|
<Text class="flex-grow">Upload database backup file</Text>
|
||||||
|
<Button size="small" onclick={upload}>Select file</Button>
|
||||||
|
</HStack>
|
||||||
|
{:else}
|
||||||
|
<HStack>
|
||||||
|
<Text class="flex-grow">Uploading...</Text>
|
||||||
|
<div class="flex-grow h-[10px] bg-gray-300 rounded-full overflow-hidden">
|
||||||
|
<div class="h-full bg-blue-600 transition-all duration-700" style="width: {uploadProgress * 100}%"></div>
|
||||||
|
</div>
|
||||||
|
</HStack>
|
||||||
|
{/if}
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{#each backups as backup (backup.filename)}
|
{#each backups as backup (backup.filename)}
|
||||||
<Card>
|
<Card>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
@@ -102,6 +152,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Button size="small" disabled={deleting.has(backup.filename)} onclick={() => restore(backup.filename)}
|
<Button size="small" disabled={deleting.has(backup.filename)} onclick={() => restore(backup.filename)}
|
||||||
>Restore</Button
|
>Restore</Button
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -43,19 +43,23 @@ export const addDummyItems = () => {
|
|||||||
|
|
||||||
export const uploadExecutionQueue = new ExecutorQueue({ concurrency: 2 });
|
export const uploadExecutionQueue = new ExecutorQueue({ concurrency: 2 });
|
||||||
|
|
||||||
|
type FilePickerParam = { multiple?: boolean; extensions?: string[] };
|
||||||
type FileUploadParam = { multiple?: boolean; albumId?: string };
|
type FileUploadParam = { multiple?: boolean; albumId?: string };
|
||||||
|
|
||||||
export const openFileUploadDialog = async (options: FileUploadParam = {}) => {
|
export const openFilePicker = async (options: FilePickerParam = {}) => {
|
||||||
const { albumId, multiple = true } = options;
|
const { multiple = true, extensions } = options;
|
||||||
const extensions = uploadManager.getExtensions();
|
|
||||||
|
|
||||||
return new Promise<string[]>((resolve, reject) => {
|
return new Promise<File[]>((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const fileSelector = document.createElement('input');
|
const fileSelector = document.createElement('input');
|
||||||
|
|
||||||
fileSelector.type = 'file';
|
fileSelector.type = 'file';
|
||||||
fileSelector.multiple = multiple;
|
fileSelector.multiple = multiple;
|
||||||
fileSelector.accept = extensions.join(',');
|
|
||||||
|
if (extensions) {
|
||||||
|
fileSelector.accept = extensions.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
fileSelector.addEventListener(
|
fileSelector.addEventListener(
|
||||||
'change',
|
'change',
|
||||||
(e: Event) => {
|
(e: Event) => {
|
||||||
@@ -63,9 +67,9 @@ export const openFileUploadDialog = async (options: FileUploadParam = {}) => {
|
|||||||
if (!target.files) {
|
if (!target.files) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const files = Array.from(target.files);
|
|
||||||
|
|
||||||
resolve(fileUploadHandler({ files, albumId }));
|
const files = Array.from(target.files);
|
||||||
|
resolve(files);
|
||||||
},
|
},
|
||||||
{ passive: true },
|
{ passive: true },
|
||||||
);
|
);
|
||||||
@@ -78,6 +82,17 @@ export const openFileUploadDialog = async (options: FileUploadParam = {}) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const openFileUploadDialog = async (options: FileUploadParam = {}) => {
|
||||||
|
const { albumId, multiple = true } = options;
|
||||||
|
const extensions = uploadManager.getExtensions();
|
||||||
|
const files = await openFilePicker({
|
||||||
|
multiple,
|
||||||
|
extensions,
|
||||||
|
});
|
||||||
|
|
||||||
|
return fileUploadHandler({ files, albumId });
|
||||||
|
};
|
||||||
|
|
||||||
type FileUploadHandlerParams = Omit<FileUploaderParams, 'deviceAssetId' | 'assetFile'> & {
|
type FileUploadHandlerParams = Omit<FileUploaderParams, 'deviceAssetId' | 'assetFile'> & {
|
||||||
files: File[];
|
files: File[];
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user