mirror of
https://github.com/zotero/zotero.git
synced 2026-10-03 02:21:49 +00:00
Pass a temp copy of dragged attachment files on Windows
Allowing only 'copy' for file attachment drags kept File Explorer from moving files out of 'storage' but locked the cursor at '+' even for moves within Zotero. Instead, provide the file via a flavor data provider that copies it to the temp directory when a target asks for it, so the drag can allow 'copyMove' again and Explorer's move only touches the copy. A copy's directory is removed once Explorer has moved the file out of it, at the next drag, or with the temp directory at shutdown.
This commit is contained in:
parent
bdb656ad5c
commit
acc4fd5ad2
3 changed files with 172 additions and 4 deletions
|
|
@ -243,3 +243,103 @@ Zotero.FileDragDataProvider.prototype = {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implements nsIFlavorDataProvider for a single dragged attachment file, handing drop targets a
|
||||
* copy in the temp directory instead of the file in storage. The copy is made only when a target
|
||||
* asks for the file, so drags within Zotero don't copy anything, and a target that moves the
|
||||
* dropped file (e.g., File Explorer on Windows for an unmodified drag) moves the copy.
|
||||
*
|
||||
* A target can still be working on the copy after the drag ends -- e.g., while a replace/skip
|
||||
* dialog is open -- and gives no signal when it's done, so a copy's directory is removed only
|
||||
* once it's empty (the target moved the file), at the start of the next drag, or with the temp
|
||||
* directory at shutdown.
|
||||
*
|
||||
* @param {String} path
|
||||
*/
|
||||
Zotero.TempFileDragDataProvider = function (path) {
|
||||
this._path = path;
|
||||
this._file = null;
|
||||
};
|
||||
|
||||
Zotero.TempFileDragDataProvider._copiedSinceDragEnd = false;
|
||||
|
||||
/**
|
||||
* Remove directories created for copies
|
||||
*
|
||||
* @param {Object} [options]
|
||||
* @param {Boolean} [options.onlyEmpty=false] - Remove only directories whose file the target has
|
||||
* moved away, which is safe while a target may still be using another copy
|
||||
*/
|
||||
Zotero.TempFileDragDataProvider.removeCopies = async function ({ onlyEmpty = false } = {}) {
|
||||
let tmpDir = Zotero.getTempDirectory().path;
|
||||
let children;
|
||||
try {
|
||||
children = await IOUtils.getChildren(tmpDir);
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.debug(e, 2);
|
||||
return;
|
||||
}
|
||||
for (let dir of children) {
|
||||
if (!/^drag(-\d+)?$/.test(PathUtils.filename(dir))) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
if (onlyEmpty) {
|
||||
if ((await IOUtils.getChildren(dir)).length) {
|
||||
continue;
|
||||
}
|
||||
// Non-recursive, so a copy made in the meantime is never removed
|
||||
await IOUtils.remove(dir);
|
||||
}
|
||||
else {
|
||||
await IOUtils.remove(dir, { recursive: true });
|
||||
}
|
||||
Zotero.debug(`Removed drag copy directory ${dir}`);
|
||||
}
|
||||
// A target may still have a copy open
|
||||
catch (e) {
|
||||
Zotero.debug(e, 2);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Call when a drag that offered files through these providers ends
|
||||
*/
|
||||
Zotero.TempFileDragDataProvider.onDragEnd = function () {
|
||||
if (!this._copiedSinceDragEnd) {
|
||||
return;
|
||||
}
|
||||
this._copiedSinceDragEnd = false;
|
||||
// An unmodified drop in File Explorer moves the copy right away, so remove the emptied
|
||||
// directory shortly after, with a later pass for a target that took longer
|
||||
for (let delay of [2000, 30000]) {
|
||||
setTimeout(() => this.removeCopies({ onlyEmpty: true }), delay);
|
||||
}
|
||||
};
|
||||
|
||||
Zotero.TempFileDragDataProvider.prototype = {
|
||||
QueryInterface: ChromeUtils.generateQI(["nsIFlavorDataProvider"]),
|
||||
|
||||
getFlavorData(_transferable, flavor, data) {
|
||||
if (flavor != 'application/x-moz-file') {
|
||||
return;
|
||||
}
|
||||
// Targets ask for the file repeatedly during a drag, so make the copy once
|
||||
if (!this._file) {
|
||||
let file = Zotero.File.pathToFile(this._path);
|
||||
// Copy into a directory of its own so that the copy keeps its filename
|
||||
let dir = Zotero.getTempDirectory();
|
||||
dir.append('drag');
|
||||
dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
|
||||
Zotero.debug(`Copying ${this._path} to ${dir.path} for drag`);
|
||||
file.copyTo(dir, null);
|
||||
Zotero.TempFileDragDataProvider._copiedSinceDragEnd = true;
|
||||
dir.append(file.leafName);
|
||||
this._file = dir;
|
||||
}
|
||||
data.value = this._file;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3442,6 +3442,11 @@ Zotero.Utilities.Internal.onDragItems = function (event, itemIDs, dragImage = ev
|
|||
.filter(file => file.exists());
|
||||
|
||||
if (files.length) {
|
||||
if (Zotero.isWin) {
|
||||
// Copies from earlier drags are no longer in use
|
||||
Zotero.TempFileDragDataProvider.removeCopies();
|
||||
}
|
||||
|
||||
// Advanced multi-file drag (with unique filenames, which otherwise happen automatically on
|
||||
// Windows but not Linux) and auxiliary snapshot file copying on macOS
|
||||
let dataProvider;
|
||||
|
|
@ -3464,15 +3469,30 @@ Zotero.Utilities.Internal.onDragItems = function (event, itemIDs, dragImage = ev
|
|||
event.dataTransfer.mozSetDataAt("text/x-moz-url", uri + '\n' + file.leafName, i);
|
||||
}
|
||||
|
||||
// Allow dragging to web targets (e.g., Gmail)
|
||||
// Allow dragging to the filesystem and web targets (e.g., Gmail). On Windows, hand
|
||||
// targets a copy in the temp directory, since File Explorer moves a dropped file for
|
||||
// an unmodified drag and would otherwise move it out of storage.
|
||||
Zotero.debug("Adding application/x-moz-file " + i);
|
||||
event.dataTransfer.mozSetDataAt("application/x-moz-file", file, i);
|
||||
event.dataTransfer.mozSetDataAt(
|
||||
"application/x-moz-file",
|
||||
Zotero.isWin ? new Zotero.TempFileDragDataProvider(files[i]) : file,
|
||||
i
|
||||
);
|
||||
|
||||
if (Zotero.isWin || Zotero.isLinux) {
|
||||
// Copy rather than move (Windows) or symlink (Linux) for an unmodified drag
|
||||
if (Zotero.isLinux) {
|
||||
// Copy rather than symlink for an unmodified drag. Drops within Zotero can still
|
||||
// move -- see LibraryTreeView::setDropEffect().
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
}
|
||||
}
|
||||
|
||||
if (Zotero.isWin) {
|
||||
event.currentTarget.addEventListener(
|
||||
'dragend',
|
||||
() => Zotero.TempFileDragDataProvider.onDragEnd(),
|
||||
{ once: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Get Quick Copy format for current URL (set via /ping from connector)
|
||||
|
|
|
|||
48
test/tests/fileDragDataProviderTest.js
Normal file
48
test/tests/fileDragDataProviderTest.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
"use strict";
|
||||
|
||||
describe("Zotero.TempFileDragDataProvider", function () {
|
||||
it("should hand out a copy of the file with the same name and remove its directory once emptied", async function () {
|
||||
var attachment = await importFileAttachment('test.pdf');
|
||||
var path = attachment.getFilePath();
|
||||
var provider = new Zotero.TempFileDragDataProvider(path);
|
||||
|
||||
var data = {};
|
||||
provider.getFlavorData(null, 'application/x-moz-file', data);
|
||||
var file = data.value.QueryInterface(Ci.nsIFile);
|
||||
var copyPath = file.path;
|
||||
var copyDir = PathUtils.parent(copyPath);
|
||||
|
||||
assert.notEqual(copyPath, path);
|
||||
assert.equal(file.leafName, PathUtils.filename(path));
|
||||
assert.isTrue(copyPath.startsWith(Zotero.getTempDirectory().path));
|
||||
assert.equal(
|
||||
await Zotero.File.getBinaryContentsAsync(copyPath),
|
||||
await Zotero.File.getBinaryContentsAsync(path)
|
||||
);
|
||||
|
||||
// Repeated requests get the same copy
|
||||
var data2 = {};
|
||||
provider.getFlavorData(null, 'application/x-moz-file', data2);
|
||||
assert.equal(data2.value.QueryInterface(Ci.nsIFile).path, copyPath);
|
||||
|
||||
// A copy the target may still be using stays
|
||||
await Zotero.TempFileDragDataProvider.removeCopies({ onlyEmpty: true });
|
||||
assert.isTrue(await IOUtils.exists(copyPath));
|
||||
|
||||
// Once the target has moved the file away, the directory goes
|
||||
await IOUtils.remove(copyPath);
|
||||
await Zotero.TempFileDragDataProvider.removeCopies({ onlyEmpty: true });
|
||||
assert.isFalse(await IOUtils.exists(copyDir));
|
||||
});
|
||||
|
||||
it("should remove leftover copies from earlier drags", async function () {
|
||||
var attachment = await importFileAttachment('test.pdf');
|
||||
var provider = new Zotero.TempFileDragDataProvider(attachment.getFilePath());
|
||||
var data = {};
|
||||
provider.getFlavorData(null, 'application/x-moz-file', data);
|
||||
var copyDir = PathUtils.parent(data.value.QueryInterface(Ci.nsIFile).path);
|
||||
|
||||
await Zotero.TempFileDragDataProvider.removeCopies();
|
||||
assert.isFalse(await IOUtils.exists(copyDir));
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue