Skip to content

Commit 080f607

Browse files
authored
Merge pull request #133 from qiniu/fix/region-config-and-mkdir
Fix bugs with transfer jobs and fix ci linux wine
2 parents b29244b + 34e5820 commit 080f607

File tree

13 files changed

+201
-41
lines changed

13 files changed

+201
-41
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ jobs:
1616
sudo dpkg --add-architecture i386
1717
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 86B72ED9
1818
sudo add-apt-repository 'deb [arch=amd64] https://mirror.mxe.cc/repos/apt focal main'
19+
sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list
1920
sudo apt -qq update
20-
sudo apt install -y --allow-downgrades libpcre2-8-0=10.34-7
21+
sudo apt install -yqq --allow-downgrades libgd3/focal libpcre2-8-0/focal libpcre2-16-0/focal libpcre2-32-0/focal libpcre2-posix2/focal
22+
sudo apt purge -yqq libmono* moby* mono* php* libgdiplus libpcre2-posix3
2123
sudo apt install -y wine32 wine64
2224
- name: test
2325
run: make i test

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "kodo-browser",
3-
"version": "1.0.19.1",
3+
"version": "1.0.19.2",
44
"license": "Apache-2.0",
55
"author": {
66
"name": "Rong Zhou",

src/common/models/job/download-job.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,9 @@ export default class DownloadJob extends TransferJob {
250250
private async startDownload(client: Adapter) {
251251
client.storageClasses = this.options.storageClasses;
252252

253+
// prevent the base directory has gone by unexpected reason.
254+
await this.checkAndCreateBaseDir(this.options.to.path);
255+
253256
// check overwrite
254257
// the reason for overwrite when `this.prog.loaded > 0` is to allow
255258
// download from breakpoint to work properly.
@@ -379,4 +382,27 @@ export default class DownloadJob extends TransferJob {
379382
}
380383
// useless?
381384
}
385+
386+
private async checkAndCreateBaseDir(filePath: string) {
387+
const baseDirPath = path.dirname(filePath);
388+
const isBaseDirValid: boolean = await fsPromises.access(
389+
baseDirPath,
390+
fsConstants.R_OK | fsConstants.W_OK | fsConstants.X_OK,
391+
)
392+
.then(() => true)
393+
.catch(() => false);
394+
if (isBaseDirValid) {
395+
return;
396+
}
397+
const isBaseDirExists: boolean = await fsPromises.access(
398+
baseDirPath,
399+
fsConstants.F_OK,
400+
)
401+
.then(() => true)
402+
.catch(() => false);
403+
if (isBaseDirExists) {
404+
throw Error(`Can't download to ${this.options.to}: Permission denied`);
405+
}
406+
await fsPromises.mkdir(baseDirPath, {recursive: true})
407+
}
382408
}

src/main/transfer-managers/download-manager.ts

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import path from "path";
2-
import fs, {promises as fsPromises} from "fs";
2+
import fs, {promises as fsPromises, constants as fsConstants} from "fs";
33

44
import lodash from "lodash";
55
import sanitizeFilename from "sanitize-filename";
@@ -232,22 +232,20 @@ export default class DownloadManager extends TransferManager<DownloadJob, Config
232232

233233
private async createDirectory(path: string): Promise<void> {
234234
try {
235-
await fsPromises.stat(path)
236-
.then(stat => {
237-
if (stat.isDirectory()) {
238-
return;
239-
}
240-
})
241-
.catch(err => {
242-
if (err.message.indexOf("EEXIST: file already exists") > -1) {
243-
return Promise.resolve();
244-
}
245-
if (err.message.indexOf("ENOENT: no such file or directory") > -1) {
246-
return fsPromises.mkdir(path);
247-
}
248-
249-
return Promise.reject(err);
250-
});
235+
const isDirectoryExists: boolean = await fsPromises.access(
236+
path,
237+
fsConstants.F_OK,
238+
)
239+
.then(() => true)
240+
.catch(() => false);
241+
if (isDirectoryExists) {
242+
return;
243+
}
244+
const stat = await fsPromises.stat(path);
245+
if (stat.isDirectory()) {
246+
return;
247+
}
248+
await fsPromises.mkdir(path);
251249
} catch (err: any) {
252250
this.config.onError?.(err);
253251
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import singleFlight from "./single-flight";
2+
3+
describe("test transfer-managers/single-flight.ts", () => {
4+
function makeWait(t: number) {
5+
return async (): Promise<void> => {
6+
return new Promise(resolve => {
7+
setTimeout(() => {
8+
resolve();
9+
}, t);
10+
});
11+
};
12+
}
13+
14+
function makeProcessWithTimestamp(t: number) {
15+
return async (): Promise<number> => {
16+
return new Promise(resolve => {
17+
setTimeout(() => {
18+
resolve(Date.now());
19+
}, t);
20+
});
21+
};
22+
}
23+
24+
function groupFlights(doneResults: number[]): number[] {
25+
return [
26+
...doneResults
27+
.reduce((acc: Map<number, number>, cur) => {
28+
const times = acc.get(cur);
29+
if (times) {
30+
acc.set(cur, times + 1);
31+
} else {
32+
acc.set(cur, 1);
33+
}
34+
return acc;
35+
}, new Map())
36+
.values()
37+
];
38+
}
39+
40+
const asyncProcess = makeProcessWithTimestamp(2000);
41+
const wait = makeWait(300);
42+
43+
it("with single flight", async () => {
44+
const singleFlightProcess = singleFlight(asyncProcess);
45+
const processPromises: Promise<number>[] = []
46+
for (let i = 0; i < 5; i++) {
47+
processPromises.push(singleFlightProcess(`key-${i % 2}`))
48+
await wait();
49+
}
50+
const doneResults = await Promise.all(processPromises);
51+
const actual = groupFlights(doneResults);
52+
// we call 5 processes.
53+
// 2 of them have same flightKey, the others 3 hava same flightKey.
54+
// so expect [2, 3]
55+
expect(actual).toEqual(expect.arrayContaining([2, 3]))
56+
});
57+
58+
it("without single flight", async () => {
59+
const processPromises: Promise<number>[] = []
60+
for (let i = 0; i < 5; i++) {
61+
processPromises.push(asyncProcess())
62+
await wait();
63+
}
64+
const doneResults = await Promise.all(processPromises);
65+
const actual = groupFlights(doneResults);
66+
expect(actual).toEqual(expect.arrayContaining([1, 1, 1, 1, 1]))
67+
});
68+
69+
it("with single flight but should tow flight with same key", async () => {
70+
const singleFlightProcess = singleFlight(asyncProcess);
71+
const processResults: number[] = []
72+
for (let i = 0; i < 2; i++) {
73+
const result = await singleFlightProcess(`key`);
74+
processResults.push(result);
75+
await wait();
76+
}
77+
const actual = groupFlights(processResults);
78+
expect(actual).toEqual(expect.arrayContaining([1, 1]))
79+
});
80+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
type AsyncFunction = (...args: any[]) => Promise<any>;
2+
3+
// TODO: Upgrade TypeScript to ≥4.5 to get a build-in `Awaited`.
4+
type Awaited<T> = T extends PromiseLike<infer U> ? U : T
5+
6+
// `Promise<Awaited<ReturnType<T>>>` is ugly to get the right type because of
7+
// the deferred resolving of conditional types involving unbound type parameters in TypeScript.
8+
// There are many issue with it, such as
9+
// https://github.com/microsoft/TypeScript/issues/43702
10+
// https://github.com/microsoft/TypeScript/issues/50251
11+
export default function singleFlight<T extends AsyncFunction>(
12+
fn: T,
13+
): (key: string, ...args: Parameters<T>) => Promise<Awaited<ReturnType<T>>> {
14+
const flightMap = new Map<string, Promise<Awaited<ReturnType<T>>>>();
15+
return function (key: string, ...args: Parameters<T>): Promise<Awaited<ReturnType<T>>> {
16+
const flight = flightMap.get(key);
17+
if (flight) {
18+
return flight;
19+
}
20+
const p = fn(...args);
21+
flightMap.set(key, p);
22+
p.then(() => {
23+
flightMap.delete(key);
24+
});
25+
return p;
26+
}
27+
}

src/main/transfer-managers/transfer-manager.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,15 +206,12 @@ export default abstract class TransferManager<Job extends TransferJob, Opt = {}>
206206
}: {
207207
matchStatus: Status[],
208208
} = {
209-
matchStatus: [],
209+
matchStatus: [Status.Running, Status.Waiting],
210210
}): void {
211211
this.jobIds
212212
.map(id => this.jobs.get(id))
213213
.forEach(job => {
214-
if (!job || ![Status.Running, Status.Waiting].includes(job.status)) {
215-
return;
216-
}
217-
if (!matchStatus.includes(job.status)){
214+
if (!job || !matchStatus.includes(job.status)){
218215
return;
219216
}
220217
job.stop();

src/main/transfer-managers/upload-manager.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {Status} from "@common/models/job/types";
1313

1414
import {MAX_MULTIPART_COUNT, MIN_MULTIPART_SIZE} from "./boundary-const";
1515
import TransferManager, {TransferManagerConfig} from "./transfer-manager";
16+
import singleFlight from "./single-flight";
1617

1718
// for walk
1819
interface StatsWithName extends Stats {
@@ -77,7 +78,9 @@ export default class UploadManager extends TransferManager<UploadJob, Config> {
7778
// if enable skip empty directory upload.
7879
const remoteDirectoryKey = path.dirname(remoteKey) + "/";
7980
if (remoteDirectoryKey !== "./" && !directoryToCreate.get(remoteDirectoryKey)) {
80-
this.createDirectory(
81+
const flightKey = destInfo.regionId + destInfo.bucketName + remoteDirectoryKey;
82+
this.createDirectoryWithSingleFlight(
83+
flightKey,
8184
qiniuClient,
8285
{
8386
region: destInfo.regionId,
@@ -95,9 +98,11 @@ export default class UploadManager extends TransferManager<UploadJob, Config> {
9598
// and in this electron version(nodejs v10.x) read the directory again
9699
// is too waste. so we create later.
97100
// if we are nodejs > v12.12,use opendir API to determine empty.
98-
if (!this.config.isSkipEmptyDirectory) {
101+
if (!this.config.isSkipEmptyDirectory && !directoryToCreate.get(remoteDirectoryKey)) {
99102
const remoteDirectoryKey = remoteKey + "/";
100-
this.createDirectory(
103+
const flightKey = destInfo.regionId + destInfo.bucketName + remoteDirectoryKey;
104+
this.createDirectoryWithSingleFlight(
105+
flightKey,
101106
qiniuClient,
102107
{
103108
region: destInfo.regionId,
@@ -134,6 +139,9 @@ export default class UploadManager extends TransferManager<UploadJob, Config> {
134139
hooks?.jobsAdded?.();
135140
}
136141

142+
/**
143+
* best to call {@link createDirectoryWithSingleFlight}
144+
*/
137145
private async createDirectory(
138146
client: Adapter,
139147
options: {
@@ -144,6 +152,16 @@ export default class UploadManager extends TransferManager<UploadJob, Config> {
144152
},
145153
) {
146154
await client.enter("createFolder", async client => {
155+
const isDirectoryExists = await client.isExists(
156+
options.region,
157+
{
158+
bucket: options.bucketName,
159+
key: options.key,
160+
},
161+
);
162+
if (isDirectoryExists) {
163+
return
164+
}
147165
await client.putObject(
148166
options.region,
149167
{
@@ -163,6 +181,8 @@ export default class UploadManager extends TransferManager<UploadJob, Config> {
163181
};
164182
}
165183

184+
private createDirectoryWithSingleFlight = singleFlight(this.createDirectory)
185+
166186
private createUploadJob(
167187
from: Required<UploadJob["options"]["from"]>,
168188
to: UploadJob["options"]["to"],

src/renderer/i18n/en-US.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ export default {
224224

225225
//files
226226
upload: "Upload",
227+
"upload.duplicated.basename": "Duplicated name in selected files or directories",
227228
"folder.create": "Directory",
228229
"folder.create.success": "Directory created successfully",
229230
"folder.name": "Name",

src/renderer/i18n/ja-JP.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ export default {
223223

224224
//files
225225
upload: "アップロード",
226+
"upload.duplicated.basename": "選択したファイルやディレクトリの名前が重複している",
226227
"folder.create": "ディレクトリ",
227228
"folder.create.success": "ディレクトリが正常に作成されました",
228229
"folder.name": "名前",

0 commit comments

Comments
 (0)