Skip to content

Commit da57c50

Browse files
authored
Merge branch 'main' into fix/download-using-bucket-from-file
2 parents cb3e968 + a16817b commit da57c50

7 files changed

Lines changed: 333 additions & 562 deletions

File tree

package-lock.json

Lines changed: 200 additions & 539 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@
3838
"dependencies": {
3939
"@dashlane/pqc-kem-kyber512-node": "1.0.0",
4040
"@inquirer/prompts": "8.5.2",
41-
"@internxt/inxt-js": "3.3.1",
41+
"@internxt/inxt-js": "3.3.5",
4242
"@internxt/lib": "1.5.1",
43-
"@internxt/sdk": "1.17.13",
44-
"@oclif/core": "4.11.14",
43+
"@internxt/sdk": "1.17.17",
44+
"@oclif/core": "4.13.0",
4545
"@oclif/plugin-autocomplete": "3.2.53",
4646
"axios": "1.18.1",
4747
"better-sqlite3": "12.11.1",
@@ -52,8 +52,8 @@
5252
"dotenv": "17.4.2",
5353
"express": "5.2.1",
5454
"express-async-handler": "1.2.0",
55-
"fast-xml-builder": "1.2.1",
56-
"fast-xml-parser": "5.9.3",
55+
"fast-xml-builder": "1.3.0",
56+
"fast-xml-parser": "5.10.1",
5757
"hash-wasm": "4.12.0",
5858
"mime-types": "3.0.2",
5959
"open": "11.0.0",
@@ -63,7 +63,7 @@
6363
"range-parser": "1.3.0",
6464
"selfsigned": "5.5.0",
6565
"tty-table": "5.0.0",
66-
"typeorm": "1.0.0",
66+
"typeorm": "1.1.0",
6767
"winston": "3.19.0"
6868
},
6969
"devDependencies": {
@@ -77,12 +77,12 @@
7777
"@types/range-parser": "^1.2.7",
7878
"@vitest/coverage-istanbul": "^4.1.10",
7979
"@vitest/spy": "^4.1.10",
80-
"eslint": "^10.6.0",
80+
"eslint": "^10.8.0",
8181
"husky": "^9.1.7",
82-
"lint-staged": "^17.0.8",
82+
"lint-staged": "^17.2.0",
8383
"nodemon": "^3.1.14",
8484
"oclif": "^4.23.27",
85-
"prettier": "^3.9.4",
85+
"prettier": "^3.9.6",
8686
"rimraf": "^6.1.3",
8787
"sql.js": "^1.14.1",
8888
"ts-node": "^10.9.2",
@@ -91,7 +91,7 @@
9191
"vitest-mock-express": "^2.2.0"
9292
},
9393
"optionalDependencies": {
94-
"sharp": "0.35.2"
94+
"sharp": "0.35.3"
9595
},
9696
"engines": {
9797
"node": "^22.13.0 || >=24.11.0"

src/services/auth.service.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,9 @@ export class AuthService {
107107
);
108108
credentialsChanged = true;
109109
} catch (error) {
110-
await ConfigService.instance.clearUser();
110+
if (this.shouldClearUserAfterRefreshError(error)) {
111+
await ConfigService.instance.clearUser();
112+
}
111113
throw error;
112114
}
113115
}
@@ -126,6 +128,38 @@ export class AuthService {
126128
return loginCreds;
127129
};
128130

131+
private readonly shouldClearUserAfterRefreshError = (error: unknown): boolean => {
132+
if (error instanceof InvalidCredentialsError || error instanceof ExpiredCredentialsError) {
133+
return true;
134+
}
135+
136+
const status = this.getHttpStatusFromError(error);
137+
if (status === undefined) {
138+
return false;
139+
}
140+
141+
return status >= 400 && status < 500 && status !== 408 && status !== 429;
142+
};
143+
144+
private readonly getHttpStatusFromError = (error: unknown): number | undefined => {
145+
if (typeof error !== 'object' || error === null) {
146+
return undefined;
147+
}
148+
149+
const response = 'response' in error ? (error as { response?: { status?: unknown } }).response : undefined;
150+
if (typeof response?.status === 'number') {
151+
return response.status;
152+
}
153+
154+
const status = 'status' in error ? (error as { status?: unknown }).status : undefined;
155+
if (typeof status === 'number') {
156+
return status;
157+
}
158+
159+
const statusCode = 'statusCode' in error ? (error as { statusCode?: unknown }).statusCode : undefined;
160+
return typeof statusCode === 'number' ? statusCode : undefined;
161+
};
162+
129163
/**
130164
* Refreshes the user tokens
131165
*

src/services/drive/drive-file.service.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,32 @@ export class DriveFileService {
3131
return driveFileItem;
3232
};
3333

34+
public replaceFile = async (uuid: string, payload: StorageTypes.FileEntryByUuid): Promise<DriveFileItem> => {
35+
const storageClient = SdkManager.instance.getStorage();
36+
const driveFile = await storageClient.replaceFile(uuid, {
37+
fileId: payload.fileId ?? '',
38+
size: payload.size,
39+
});
40+
41+
const driveFileItem: DriveFileItem = {
42+
itemType: 'file',
43+
name: driveFile.plainName ?? driveFile.name,
44+
uuid: driveFile.uuid,
45+
size: driveFile.size,
46+
bucket: driveFile.bucket,
47+
createdAt: new Date(driveFile.createdAt),
48+
updatedAt: new Date(driveFile.updatedAt),
49+
fileId: driveFile.fileId ?? null,
50+
type: driveFile.type ?? null,
51+
status: driveFile.status as DriveFileItem['status'],
52+
folderUuid: driveFile.folderUuid,
53+
creationTime: new Date(driveFile.creationTime ?? driveFile.createdAt),
54+
modificationTime: new Date(driveFile.modificationTime ?? driveFile.updatedAt),
55+
};
56+
57+
return driveFileItem;
58+
};
59+
3460
private readonly createDriveFileEntry = async (
3561
payload: StorageTypes.FileEntryByUuid,
3662
): Promise<StorageTypes.DriveFileData> => {

src/webdav/handlers/PUT.handler.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,6 @@ export class PUTRequestHandler implements WebDavMethodHandler {
5252
webdavLogger.info(
5353
`[PUT] File '${resource.name}' already exists in '${resource.path.dir}', it will be replaced...`,
5454
);
55-
try {
56-
await WebDavUtils.deleteOrTrashItem(driveFileItem);
57-
await DriveItemRepository.instance.delete([driveFileItem.uuid]);
58-
} catch {
59-
//noop
60-
}
6155
}
6256

6357
const { user } = await AuthService.instance.getAuthDetails();
@@ -104,15 +98,37 @@ export class PUTRequestHandler implements WebDavMethodHandler {
10498
}
10599

106100
const driveTimer = CLIUtils.timer();
107-
const file = await DriveFileService.instance.createFile({
101+
const filePayload = {
108102
plainName: resource.path.name,
109103
type: fileType,
110104
size: contentLength,
111105
folderUuid: parentDriveFolderItem.uuid,
112106
fileId,
113107
bucket,
114108
encryptVersion: EncryptionVersion.Aes03,
115-
});
109+
};
110+
111+
let file;
112+
if (driveFileItem?.itemType === 'file' && contentLength > 0) {
113+
try {
114+
file = await DriveFileService.instance.replaceFile(driveFileItem.uuid, filePayload);
115+
} catch (error) {
116+
webdavLogger.warn(
117+
`[PUT] File replace failed for '${resource.url}', falling back to delete and create: ${
118+
error instanceof Error ? error.message : String(error)
119+
}`,
120+
);
121+
await WebDavUtils.deleteOrTrashItem(driveFileItem);
122+
await DriveItemRepository.instance.delete([driveFileItem.uuid]);
123+
file = await DriveFileService.instance.createFile(filePayload);
124+
}
125+
} else {
126+
if (driveFileItem?.itemType === 'file') {
127+
await WebDavUtils.deleteOrTrashItem(driveFileItem);
128+
await DriveItemRepository.instance.delete([driveFileItem.uuid]);
129+
}
130+
file = await DriveFileService.instance.createFile(filePayload);
131+
}
116132
timings.driveUpload = driveTimer.stop();
117133

118134
await DriveItemRepository.instance.createOrUpdate([

test/services/auth.service.test.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,12 +217,14 @@ describe('Auth service', () => {
217217
expect(refreshTokensStub).toHaveBeenCalledOnce();
218218
});
219219

220-
test('when the token refresh fails, then stored credentials are cleared and an error is thrown', async () => {
220+
test('when the token refresh fails with an auth error, then stored credentials are cleared and an error is thrown', async () => {
221221
const sut = AuthService.instance;
222222

223223
const mockTokenStatus = TokenStatus.REFRESH_REQUIRED;
224224

225-
const oldTokenError = new Error('Old token version detected');
225+
const oldTokenError = Object.assign(new Error('Old token version detected'), {
226+
response: { status: 401 },
227+
});
226228

227229
vi.spyOn(ConfigService.instance, 'readUser').mockResolvedValue(UserCredentialsFixture);
228230
vi.spyOn(ValidationService.instance, 'validateTokenAndCheckExpiration').mockImplementationOnce(
@@ -236,4 +238,26 @@ describe('Auth service', () => {
236238
expect(refreshTokenStub).toHaveBeenCalledOnce();
237239
expect(clearUserStub).toHaveBeenCalledOnce();
238240
});
241+
242+
test('when the token refresh fails with a retryable server error, then stored credentials are preserved', async () => {
243+
const sut = AuthService.instance;
244+
245+
const mockTokenStatus = TokenStatus.REFRESH_REQUIRED;
246+
247+
const retryableError = Object.assign(new Error('Service unavailable'), {
248+
response: { status: 503 },
249+
});
250+
251+
vi.spyOn(ConfigService.instance, 'readUser').mockResolvedValue(UserCredentialsFixture);
252+
vi.spyOn(ValidationService.instance, 'validateTokenAndCheckExpiration').mockImplementationOnce(
253+
() => mockTokenStatus,
254+
);
255+
vi.spyOn(ValidationService.instance, 'validateMnemonic').mockReturnValue(true);
256+
const refreshTokenStub = vi.spyOn(sut, 'refreshUserToken').mockRejectedValue(retryableError);
257+
const clearUserStub = vi.spyOn(ConfigService.instance, 'clearUser').mockResolvedValue();
258+
259+
await expect(() => sut.getAuthDetails()).rejects.toThrow(retryableError);
260+
expect(refreshTokenStub).toHaveBeenCalledOnce();
261+
expect(clearUserStub).not.toHaveBeenCalled();
262+
});
239263
});

test/webdav/handlers/PUT.handler.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ describe('PUT request handler', () => {
168168
.mockResolvedValue(UserCredentialsFixture);
169169
const uploadStub = vi.spyOn(networkFacade, 'uploadFile').mockResolvedValue('uploaded-file-id');
170170
const createDriveFileStub = vi.spyOn(DriveFileService.instance, 'createFile').mockResolvedValue(fileFixture);
171+
const replaceDriveFileStub = vi.spyOn(DriveFileService.instance, 'replaceFile').mockResolvedValue(fileFixture);
171172

172173
await sut.handle(request, response);
173174
expect(response.status).toHaveBeenCalledWith(204);
@@ -176,7 +177,16 @@ describe('PUT request handler', () => {
176177
expect(getDriveFolderFromResourceStub).toHaveBeenCalledOnce();
177178
expect(getAuthDetailsStub).toHaveBeenCalledOnce();
178179
expect(uploadStub).toHaveBeenCalledOnce();
179-
expect(createDriveFileStub).toHaveBeenCalledOnce();
180-
expect(deleteDriveFileStub).toHaveBeenCalledOnce();
180+
expect(replaceDriveFileStub).toHaveBeenCalledWith(
181+
fileFixture.uuid,
182+
expect.objectContaining({
183+
fileId: 'uploaded-file-id',
184+
folderUuid: folderFixture.uuid,
185+
plainName: requestedFileResource.path.name,
186+
size: 100,
187+
}),
188+
);
189+
expect(createDriveFileStub).not.toHaveBeenCalled();
190+
expect(deleteDriveFileStub).not.toHaveBeenCalled();
181191
});
182192
});

0 commit comments

Comments
 (0)