Skip to content

Commit

Permalink
bulk delete
Browse files Browse the repository at this point in the history
  • Loading branch information
eldertek committed Dec 26, 2024
1 parent 9f98d93 commit 8f80a04
Show file tree
Hide file tree
Showing 10 changed files with 583 additions and 120 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## 1.3.1 - 2024-12-26
### Added
- New bulk deletion feature allowing users to delete multiple duplicates at once
### Changed
- Improved documentation for the cleanup background job to clarify it only affects the database
- Updated settings interface with clearer descriptions about database cleanup operations
Expand Down
15 changes: 12 additions & 3 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
<?php
return [
'routes' => [
// Page routes
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
['name' => 'duplicate_api#list', 'url' => '/api/duplicates/{type}', 'verb' => 'GET', 'requirements' => ['page' => '\d+', 'limit' => '\d+']],
['name' => 'duplicate_api#acknowledge', 'url' => '/api/duplicates/acknowledge/{hash}', 'verb' => 'POST'],
['name' => 'duplicate_api#unacknowledge', 'url' => '/api/duplicates/unacknowledge/{hash}', 'verb' => 'POST'],

// Duplicate routes
['name' => 'duplicate_api#find', 'url' => '/api/duplicates/find', 'verb' => 'POST'],
['name' => 'duplicate_api#clear', 'url' => '/api/duplicates/clear', 'verb' => 'POST'],
['name' => 'duplicate_api#list', 'url' => '/api/duplicates/{type}', 'verb' => 'GET', 'requirements' => ['page' => '\d+', 'limit' => '\d+', 'onlyNonProtected' => 'true|false']],
['name' => 'duplicate_api#acknowledge', 'url' => '/api/duplicates/acknowledge/{hash}', 'verb' => 'POST'],
['name' => 'duplicate_api#unacknowledge', 'url' => '/api/duplicates/unacknowledge/{hash}', 'verb' => 'POST'],

// Settings routes
['name' => 'settings_api#list', 'url' => '/api/settings', 'verb' => 'GET'],
['name' => 'settings_api#save', 'url' => '/api/settings/{key}/{value}', 'verb' => 'POST'],

// Origin folder routes
['name' => 'origin_folder_api#index', 'url' => '/api/origin-folders', 'verb' => 'GET'],
['name' => 'origin_folder_api#create', 'url' => '/api/origin-folders', 'verb' => 'POST'],
['name' => 'origin_folder_api#destroy', 'url' => '/api/origin-folders/{id}', 'verb' => 'DELETE'],

// File routes
['name' => 'file_api#delete', 'url' => '/api/files/delete', 'verb' => 'POST'],
],
];
2 changes: 1 addition & 1 deletion js/duplicatefinder-main.js

Large diffs are not rendered by default.

27 changes: 22 additions & 5 deletions lib/Controller/DuplicateApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,34 @@ public function __construct(
* @NoAdminRequired
* @NoCSRFRequired
*/
public function list(int $page = 1, int $limit = 30, string $type = 'unacknowledged'): DataResponse
public function list(int $page = 1, int $limit = 30, string $type = 'unacknowledged', bool $onlyNonProtected = false): DataResponse
{
try {
$duplicates = $this->fileDuplicateService->findAll($type, $this->getUserId(), $page, $limit, true);
$totalItems = $this->fileDuplicateService->getTotalCount($type);
$totalPages = ceil($totalItems / $limit);

if ($onlyNonProtected) {
// For each duplicate group, keep only non-protected files
foreach ($duplicates['entities'] as $key => $duplicate) {
$nonProtectedFiles = array_filter($duplicate->getFiles(), function($file) {
return !$file->getIsInOriginFolder();
});

// If no files remain after filtering, remove the group
if (empty($nonProtectedFiles)) {
unset($duplicates['entities'][$key]);
continue;
}

// Update the duplicate group with only non-protected files
$duplicate->setFiles(array_values($nonProtectedFiles));
}
}

$data = [
'status' => 'success',
'entities' => $duplicates['entities'],
'entities' => array_values($duplicates['entities']),
'pagination' => [
'currentPage' => $page,
'totalPages' => $totalPages,
Expand Down Expand Up @@ -123,9 +141,8 @@ public function find(): DataResponse
});
return new DataResponse(['status' => 'success']);
} catch (\Exception $e) {
$this->logger->error('A unknown exception occured', ['app' => Application::ID, 'exception' => $e]);
$this->logger->error('A unknown exception occurred', ['app' => Application::ID, 'exception' => $e]);
return new DataResponse(['status' => 'error', 'message' => $e->getMessage()]);
}
}

}
}
40 changes: 32 additions & 8 deletions lib/Controller/FileApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,51 @@ public function __construct(
}

/**
* Delete a file
* Delete one or multiple files
*
* @NoAdminRequired
* @NoCSRFRequired
* @return JSONResponse
*/
public function delete(): JSONResponse {
$path = $this->request->getParam('path');
if (empty($path)) {
$paths = $this->request->getParam('paths');

if (empty($path) && empty($paths)) {
return new JSONResponse(
['error' => 'Path parameter is required'],
['error' => 'Path or paths parameter is required'],
Http::STATUS_BAD_REQUEST
);
}

try {
$this->logger->debug('Attempting to delete file: {path}', ['path' => $path]);
$this->service->deleteFile($this->userId, $path);
$this->logger->info('Successfully deleted file: {path}', ['path' => $path]);
$results = ['success' => [], 'errors' => []];

return new JSONResponse(['status' => 'success']);
try {
if (!empty($paths) && is_array($paths)) {
foreach ($paths as $singlePath) {
try {
$this->logger->debug('Attempting to delete file: {path}', ['path' => $singlePath]);
$this->service->deleteFile($this->userId, $singlePath);
$this->logger->info('Successfully deleted file: {path}', ['path' => $singlePath]);
$results['success'][] = $singlePath;
} catch (Exception $e) {
$this->logger->error('Error deleting file: {error}', [
'error' => $e->getMessage(),
'path' => $singlePath
]);
$results['errors'][] = [
'path' => $singlePath,
'error' => $e->getMessage()
];
}
}
return new JSONResponse($results);
} else {
$this->logger->debug('Attempting to delete single file: {path}', ['path' => $path]);
$this->service->deleteFile($this->userId, $path);
$this->logger->info('Successfully deleted file: {path}', ['path' => $path]);
return new JSONResponse(['status' => 'success']);
}
} catch (Exception $e) {
$this->logger->error('Error deleting file: {error}', [
'error' => $e->getMessage(),
Expand Down
17 changes: 2 additions & 15 deletions lib/Service/FileInfoService.php
Original file line number Diff line number Diff line change
Expand Up @@ -302,23 +302,10 @@ public function scanFiles(
private function handleLockedFile(string $path, ?OutputInterface $output): void
{
try {
// Get the file node
$node = $this->rootFolder->get($path);
$storage = $node->getStorage();

if ($storage instanceof IStorage) {
// Try to release the lock at the storage level
$storage->unlockFile($path, ILockingProvider::LOCK_SHARED);
CMDUtils::showIfOutputIsPresent(
"Released storage-level lock for file: $path",
$output
);
}

// Try to release the lock at the application level
// Release the lock using the locking provider
$this->lockingProvider->releaseAll($path, ILockingProvider::LOCK_SHARED);
CMDUtils::showIfOutputIsPresent(
"Released application-level lock for file: $path",
"Released lock for file: $path",
$output
);

Expand Down
68 changes: 55 additions & 13 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,23 @@
<template v-else>
<DuplicateNavigation v-if="acknowledgedDuplicates.length > 0 || unacknowledgedDuplicates.length > 0"
:acknowledged-duplicates="acknowledgedDuplicates" :unacknowledged-duplicates="unacknowledgedDuplicates"
:currentDuplicateId="currentDuplicate?.id"
:acknowledgedPagination="acknowledgedPagination"
:unacknowledgedPagination="unacknowledgedPagination"
:activeView="activeView"
@open-duplicate="openDuplicate"
@open-settings="settingsOpen = true" />
@open-settings="settingsOpen = true"
@open-bulk-delete="openBulkDelete"
@update-acknowledged-duplicates="updateAcknowledgedDuplicates"
@update-unacknowledged-duplicates="updateUnacknowledgedDuplicates" />
<NcAppContent>
<DuplicateDetails ref="duplicateDetails" :duplicate="currentDuplicate" @duplicate-resolved="handleDuplicateResolved"
@duplicateUpdated="updateDuplicate" />
<template v-if="activeView === 'bulk-delete'">
<BulkDeletionSettings @duplicates-deleted="refreshDuplicates" />
</template>
<template v-else>
<DuplicateDetails ref="duplicateDetails" :duplicate="currentDuplicate" @duplicate-resolved="handleDuplicateResolved"
@duplicateUpdated="updateDuplicate" />
</template>
</NcAppContent>

<NcAppSettingsDialog
Expand All @@ -23,15 +35,6 @@
</template>
<OriginFoldersSettings />
</NcAppSettingsSection>

<NcAppSettingsSection
id="bulk-deletion"
:name="t('duplicatefinder', 'Bulk Deletion')">
<template #icon>
<Delete :size="20" />
</template>
<BulkDeletionSettings @duplicates-deleted="refreshDuplicates" />
</NcAppSettingsSection>
</NcAppSettingsDialog>
</template>
</NcContent>
Expand Down Expand Up @@ -73,8 +76,20 @@ export default {
page: 1,
limit: 50,
settingsOpen: false,
activeView: 'details',
acknowledgedPagination: {
currentPage: 1,
totalPages: 1
},
unacknowledgedPagination: {
currentPage: 1,
totalPages: 1
}
};
},
async created() {
await this.loadInitialData()
},
methods: {
handleDuplicateResolved({ duplicate, type }) {
console.log('App: Handling duplicate-resolved event:', { duplicate, type });
Expand Down Expand Up @@ -179,7 +194,8 @@ export default {
}
},
async openDuplicate(duplicate) {
this.currentDuplicate = duplicate;
this.activeView = 'details'
this.currentDuplicate = duplicate
},
async refreshDuplicates() {
this.isLoading = true;
Expand Down Expand Up @@ -208,6 +224,32 @@ export default {
this.isLoading = false;
}
},
async loadInitialData() {
try {
const [acknowledgedResponse, unacknowledgedResponse] = await Promise.all([
fetchDuplicates('acknowledged', 50, 1),
fetchDuplicates('unacknowledged', 50, 1)
])
this.acknowledgedDuplicates = acknowledgedResponse.entities
this.unacknowledgedDuplicates = unacknowledgedResponse.entities
this.acknowledgedPagination = acknowledgedResponse.pagination
this.unacknowledgedPagination = unacknowledgedResponse.pagination
} catch (error) {
console.error('Error loading initial data:', error)
showError(t('duplicatefinder', 'Could not load duplicates'))
}
},
updateAcknowledgedDuplicates(newDuplicates) {
this.acknowledgedDuplicates = [...this.acknowledgedDuplicates, ...newDuplicates]
},
updateUnacknowledgedDuplicates(newDuplicates) {
this.unacknowledgedDuplicates = [...this.unacknowledgedDuplicates, ...newDuplicates]
},
openBulkDelete() {
this.activeView = 'bulk-delete'
this.currentDuplicate = null
}
},
mounted() {
// Fetch initial duplicates
Expand Down
Loading

0 comments on commit 8f80a04

Please sign in to comment.