Skip to content

Commit

Permalink
Merge branch 'AiroPi-master'
Browse files Browse the repository at this point in the history
  • Loading branch information
pulsejet committed Jan 4, 2025
2 parents c99dc1e + d07a225 commit ba8a77a
Show file tree
Hide file tree
Showing 3 changed files with 241 additions and 11 deletions.
45 changes: 41 additions & 4 deletions src/components/modal/MoveToFolderModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { FilePickerType } from '@nextcloud/dialogs';
import { showInfo } from '@nextcloud/dialogs';
const NcProgressBar = () => import('@nextcloud/vue/dist/Components/NcProgressBar.js');
Expand Down Expand Up @@ -61,13 +60,47 @@ export default defineComponent({
},
async chooseFolderPath() {
enum Mode {
Move = 1,
Copy = 2,
Organise = 3,
}
let mode: Mode = Mode.Move as Mode;
let destination = await utils.chooseNcFolder(
this.t('memories', 'Choose a folder'),
this.config.folders_path,
FilePickerType.Move,
() => [
{
label: 'Move and organise',
callback: () => (mode = Mode.Organise),
},
{
label: 'Copy',
callback: () => (mode = Mode.Copy),
},
{
label: 'Move',
type: 'primary',
callback: () => (mode = Mode.Move),
},
],
);
// Fails if the target exists, same behavior with Nextcloud files implementation.
const gen = dav.movePhotos(this.photos, destination, false);
let gen = (() => {
switch (mode) {
case Mode.Organise: {
return dav.movePhotosByDate(this.photos, destination, false);
}
case Mode.Copy: {
return dav.copyPhotos(this.photos, destination, false);
}
case Mode.Move: {
return dav.movePhotos(this.photos, destination, false);
}
}
})();
this.show = true;
for await (const fids of gen) {
Expand All @@ -76,7 +109,11 @@ export default defineComponent({
}
const n = this.photosDone;
showInfo(this.n('memories', '{n} item moved to folder', '{n} items moved to folder', n, { n }));
if (mode === Mode.Copy) {
showInfo(this.n('memories', '{n} item copied to folder', '{n} items copied to folder', n, { n }));
} else {
showInfo(this.n('memories', '{n} item moved to folder', '{n} items moved to folder', n, { n }));
}
this.close();
},
},
Expand Down
175 changes: 174 additions & 1 deletion src/services/dav/base.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { showError } from '@nextcloud/dialogs';
import axios from '@nextcloud/axios';
import { showError } from '@nextcloud/dialogs';

import { getAlbumFileInfos } from './albums';
import client, { remotePath } from './client';
Expand Down Expand Up @@ -285,6 +285,75 @@ export async function* deletePhotos(photos: IPhoto[], confirm: boolean = true) {
yield* runInParallel(calls, 10);
}

/**
* copy all files in a given list of Ids to given destination
*
* @param photos list of photos to copy
* @param destination to copy photos into
* @param overwrite behaviour if the target exists. `true` overwrites, `false` fails.
* @returns list of file ids that were copied
*/
export async function* copyPhotos(photos: IPhoto[], destination: string, overwrite: boolean) {
if (photos.length === 0) {
return;
}

// Set absolute target path
const prefixPath = `files/${utils.uid}`;
let targetPath = prefixPath + destination;
if (!targetPath.endsWith('/')) {
targetPath += '/';
}

// Also copy the stack files
photos = await extendWithStack(photos);
const fileIdsSet = new Set(photos.map((p) => p.fileid));

// Get files data
let fileInfos: IFileInfo[] = [];
try {
fileInfos = await getFiles(photos);
} catch (e) {
console.error('Failed to get file info for files to copy', photos, e);
showError(t('memories', 'Failed to copy files.'));
return;
}

// Copy each file
fileInfos = fileInfos.filter((f) => fileIdsSet.has(f.fileid));
const calls = fileInfos.map((fileInfo) => async () => {
try {
await client.copyFile(
fileInfo.originalFilename,
targetPath + fileInfo.basename,
// @ts-ignore - https://github.com/perry-mitchell/webdav-client/issues/329
{ headers: { Overwrite: overwrite ? 'T' : 'F' } },
);
return fileInfo.fileid;
} catch (error) {
console.error('Failed to copy', fileInfo, error);
if (error.response?.status === 412) {
// Precondition failed (only if `overwrite` flag set to false)
showError(
t('memories', 'Could not copy {fileName}, target exists.', {
fileName: fileInfo.filename,
}),
);
return 0;
}

showError(
t('memories', 'Failed to copy {fileName}.', {
fileName: fileInfo.filename,
}),
);
return 0;
}
});

yield* runInParallel(calls, 10);
}

/**
* Move all files in a given list of Ids to given destination
*
Expand Down Expand Up @@ -354,6 +423,110 @@ export async function* movePhotos(photos: IPhoto[], destination: string, overwri
yield* runInParallel(calls, 10);
}

/**
* Move multiple files in given lists of Ids to corresponding destinations in a year/month folder structure.
*
* @param photos list of photos to move
* @param destination to move photos into
* @param overwrite behaviour if the target exists. `true` overwrites, `false` fails.
* @returns list of file ids that were moved
*/
export async function* movePhotosByDate(photos: IPhoto[], destination: string, overwrite: boolean) {
if (photos.length === 0) {
return;
}

// Set absolute target path
const prefixPath = `files/${utils.uid}`;
destination = `${prefixPath}/${destination}`;
const datePaths: Map<string, Set<string>> = new Map(); // {'year': {'month1', 'month2'}}

photos = await extendWithStack(photos);
const fileIdsSet = new Set(photos.map((p) => p.fileid));

let fileInfos: IFileInfo[] = [];

try {
fileInfos = await getFiles(photos);
} catch (e) {
console.error('Failed to get file info for files to move', photos, e);
showError(t('memories', 'Failed to move files.'));
return;
}

const moveDirectives: Array<[string, IFileInfo]> = new Array();

photos.forEach((photo, i) => {
if (!fileIdsSet.has(fileInfos[i].fileid)) {
return;
}

const date = utils.dayIdToDate(photo.dayid);

const year = date.getFullYear().toString();
const month = String(date.getMonth() + 1).padStart(2, '0');

const months = datePaths.get(year) || new Set();
months.add(month);
datePaths.set(year, months);

const datePath = `${destination}/${year}/${month}`;

moveDirectives.push([datePath, fileInfos[i]]);
});

async function createIfNotExist(directory: string, subDirectories: Iterable<string>) {
let existing = await client.getDirectoryContents(directory);
if ('data' in existing) {
existing = existing.data;
}
existing = existing.filter((f) => f.type === 'directory');
for (const sub of subDirectories) {
if (!existing.some((f) => f.basename === sub)) {
await client.createDirectory(`${directory}/${sub}`);
}
}
}

await createIfNotExist(destination, datePaths.keys());
for (const [year, months] of datePaths) {
await createIfNotExist(`${destination}/${year}`, months);
}

// Move each file
const calls = moveDirectives.map(([targetPath, fileInfo]) => async () => {
try {
await client.moveFile(
fileInfo.originalFilename,
`${targetPath}/${fileInfo.basename}`,
// @ts-ignore - https://github.com/perry-mitchell/webdav-client/issues/329
{ headers: { Overwrite: overwrite ? 'T' : 'F' } },
);
return fileInfo.fileid;
} catch (error) {
console.error('Failed to move', fileInfo, error);
if (error.response?.status === 412) {
// Precondition failed (only if `overwrite` flag set to false)
showError(
t('memories', 'Could not move {fileName}, target exists.', {
fileName: fileInfo.filename,
}),
);
return 0;
}

showError(
t('memories', 'Failed to move {fileName}.', {
fileName: fileInfo.filename,
}),
);
return 0;
}
});

yield* runInParallel(calls, 10);
}

/**
* Fill the imageInfo attributes of the given photos
*
Expand Down
32 changes: 26 additions & 6 deletions src/services/utils/dialog.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { FilePickerType, getFilePickerBuilder } from '@nextcloud/dialogs';
import { showError } from '@nextcloud/dialogs';
import type { IFilePickerButton } from '@nextcloud/dialogs';
import { getFilePickerBuilder, showError } from '@nextcloud/dialogs';
import type { Node } from '@nextcloud/files';

import { translate as t, translatePlural as n } from '@services/l10n';
import { translatePlural as n, translate as t } from '@services/l10n';
import { bus } from './event-bus';
import { fragment } from './fragment';

// https://github.com/nextcloud/server/blob/4b7ec0a0c18d4e2007565dc28ee214814940161e/core/src/OC/dialogs.js
const oc_dialogs = (<any>OC).dialogs;

type IFilePickerButtonFactory = (
selectedNodes: Node[],
currentPath: string,
currentView: string,
) => IFilePickerButton[];

type ConfirmOptions = {
/** Title of dialog */
title?: string;
Expand Down Expand Up @@ -149,23 +156,36 @@ export async function prompt(opts: PromptOptions): Promise<string | null> {
);
}

/** Default button factory for the file picker */
function chooseButtonFactory(nodes: Node[]): IFilePickerButton[] {
const fileName = nodes?.[0]?.attributes?.displayName || nodes?.[0]?.basename;
let label = nodes.length === 1 ? t('memories', 'Choose {file}', { file: fileName }) : t('memories', 'Choose');
return [
{
callback: () => {},
type: 'primary',
label: label,
},
];
}

/**
* Choose a folder using the NC file picker
*
* @param title Title of the file picker
* @param initial Initial path
* @param type Type of the file picker
* @param buttonFactory Buttons factory
*
* @returns The path of the chosen folder
*/
export async function chooseNcFolder(
title: string,
initial: string = '/',
type: FilePickerType = FilePickerType.Choose,
buttonFactory: IFilePickerButtonFactory = chooseButtonFactory,
): Promise<string> {
const picker = getFilePickerBuilder(title)
.setMultiSelect(false)
.setType(type)
.setButtonFactory(buttonFactory)
.addMimeTypeFilter('httpd/unix-directory')
.allowDirectories()
.startAt(initial)
Expand Down

0 comments on commit ba8a77a

Please sign in to comment.