@t4h.framework/dev 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/models/SftpClient.d.ts +7 -0
- package/dist/models/SftpClient.js +25 -0
- package/dist/models/SftpClient.js.map +1 -0
- package/dist/models/SftpClientCache.d.ts +6 -0
- package/dist/models/SftpClientCache.js +34 -0
- package/dist/models/SftpClientCache.js.map +1 -0
- package/dist/models/claims/DevSftpClaim.d.ts +4 -3
- package/dist/models/claims/DevSftpClaim.js +78 -81
- package/dist/models/claims/DevSftpClaim.js.map +1 -1
- package/dist/tools/get-sftp-config-from-connection-string.d.ts +2 -0
- package/dist/tools/get-sftp-config-from-connection-string.js +25 -0
- package/dist/tools/get-sftp-config-from-connection-string.js.map +1 -0
- package/dist/tools/json-codec.d.ts +9 -1
- package/dist/tools/json-codec.js +9 -2
- package/dist/tools/json-codec.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { PassThrough } from 'node:stream';
|
|
2
|
+
import BaseSftpClient from 'ssh2-sftp-client';
|
|
3
|
+
export declare class SftpClient extends BaseSftpClient {
|
|
4
|
+
static connect(connectionString: string): Promise<SftpClient>;
|
|
5
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
6
|
+
static stream(connectionString: string, filepath: string): Promise<PassThrough>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { PassThrough } from 'node:stream';
|
|
2
|
+
import BaseSftpClient from 'ssh2-sftp-client';
|
|
3
|
+
import { getSftpConfigFromConnectionString } from '../tools/get-sftp-config-from-connection-string.js';
|
|
4
|
+
export class SftpClient extends BaseSftpClient {
|
|
5
|
+
static async connect(connectionString) {
|
|
6
|
+
const client = new SftpClient(connectionString);
|
|
7
|
+
await client.connect(await getSftpConfigFromConnectionString(connectionString));
|
|
8
|
+
return client;
|
|
9
|
+
}
|
|
10
|
+
async [Symbol.asyncDispose]() {
|
|
11
|
+
await this.end().catch(error => console.error(error));
|
|
12
|
+
}
|
|
13
|
+
static async stream(connectionString, filepath) {
|
|
14
|
+
const client = await SftpClient.connect(connectionString);
|
|
15
|
+
const stream = new PassThrough();
|
|
16
|
+
void client
|
|
17
|
+
.get(filepath, stream)
|
|
18
|
+
.catch((error) => stream.destroy(error))
|
|
19
|
+
.finally(() => {
|
|
20
|
+
void client.end().catch(error => console.error(error));
|
|
21
|
+
});
|
|
22
|
+
return stream;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=SftpClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SftpClient.js","sourceRoot":"","sources":["../../src/models/SftpClient.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAEzC,OAAO,cAAc,MAAM,kBAAkB,CAAA;AAE7C,OAAO,EAAE,iCAAiC,EAAE,MAAM,oDAAoD,CAAA;AAEtG,MAAM,OAAO,UAAW,SAAQ,cAAc;IAC5C,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,gBAAwB;QAC3C,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,gBAAgB,CAAC,CAAA;QAE/C,MAAM,MAAM,CAAC,OAAO,CAClB,MAAM,iCAAiC,CAAC,gBAAgB,CAAC,CAC1D,CAAA;QAED,OAAO,MAAM,CAAA;IACf,CAAC;IAEM,KAAK,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC;QAChC,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAA;IACvD,CAAC;IAEM,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,gBAAwB,EAAE,QAAgB;QACnE,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAA;QAEzD,MAAM,MAAM,GAAG,IAAI,WAAW,EAAE,CAAA;QAEhC,KAAK,MAAM;aACR,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC;aACrB,KAAK,CAAC,CAAC,KAAY,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;aAC9C,OAAO,CAAC,GAAG,EAAE;YACZ,KAAK,MAAM,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAA;QACxD,CAAC,CAAC,CAAA;QAEJ,OAAO,MAAM,CAAA;IACf,CAAC;CACF"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { SftpClient } from './SftpClient.js';
|
|
2
|
+
export type SftpClientConnector = (connectionString: string, onConnectionLost: () => void) => Promise<SftpClient>;
|
|
3
|
+
export declare function evictSftpClientFromCache(connectionString: string): void;
|
|
4
|
+
export declare function getCachedSftpClient(connectionString: string, connect: SftpClientConnector): Promise<SftpClient>;
|
|
5
|
+
export declare function clearSftpClientCache(): void;
|
|
6
|
+
export declare function getSftpClientCacheSize(): number;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { LRUCache } from 'lru-cache';
|
|
2
|
+
import { SftpClient } from './SftpClient.js';
|
|
3
|
+
const DEFAULT_MAX_CLIENTS = 16;
|
|
4
|
+
const DEFAULT_TTL_MS = 60_000;
|
|
5
|
+
const clientCache = new LRUCache({
|
|
6
|
+
max: DEFAULT_MAX_CLIENTS,
|
|
7
|
+
ttl: DEFAULT_TTL_MS,
|
|
8
|
+
ttlAutopurge: true,
|
|
9
|
+
dispose: client => {
|
|
10
|
+
void client.end().catch(() => { });
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
export function evictSftpClientFromCache(connectionString) {
|
|
14
|
+
if (!clientCache.has(connectionString))
|
|
15
|
+
return;
|
|
16
|
+
clientCache.delete(connectionString);
|
|
17
|
+
}
|
|
18
|
+
export async function getCachedSftpClient(connectionString, connect) {
|
|
19
|
+
const cached = clientCache.get(connectionString);
|
|
20
|
+
if (cached)
|
|
21
|
+
return cached;
|
|
22
|
+
const client = await connect(connectionString, () => {
|
|
23
|
+
evictSftpClientFromCache(connectionString);
|
|
24
|
+
});
|
|
25
|
+
clientCache.set(connectionString, client);
|
|
26
|
+
return client;
|
|
27
|
+
}
|
|
28
|
+
export function clearSftpClientCache() {
|
|
29
|
+
clientCache.clear();
|
|
30
|
+
}
|
|
31
|
+
export function getSftpClientCacheSize() {
|
|
32
|
+
return clientCache.size;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=SftpClientCache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SftpClientCache.js","sourceRoot":"","sources":["../../src/models/SftpClientCache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAEpC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAE5C,MAAM,mBAAmB,GAAG,EAAE,CAAA;AAC9B,MAAM,cAAc,GAAG,MAAM,CAAA;AAO7B,MAAM,WAAW,GAAG,IAAI,QAAQ,CAAqB;IACnD,GAAG,EAAE,mBAAmB;IACxB,GAAG,EAAE,cAAc;IACnB,YAAY,EAAE,IAAI;IAClB,OAAO,EAAE,MAAM,CAAC,EAAE;QAChB,KAAK,MAAM,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;IACnC,CAAC;CACF,CAAC,CAAA;AAEF,MAAM,UAAU,wBAAwB,CAAC,gBAAwB;IAC/D,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,gBAAgB,CAAC;QAAE,OAAM;IAE9C,WAAW,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAA;AACtC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,gBAAwB,EACxB,OAA4B;IAE5B,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAA;IAEhD,IAAI,MAAM;QAAE,OAAO,MAAM,CAAA;IAEzB,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,gBAAgB,EAAE,GAAG,EAAE;QAClD,wBAAwB,CAAC,gBAAgB,CAAC,CAAA;IAC5C,CAAC,CAAC,CAAA;IAEF,WAAW,CAAC,GAAG,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAA;IAEzC,OAAO,MAAM,CAAA;AACf,CAAC;AAED,MAAM,UAAU,oBAAoB;IAClC,WAAW,CAAC,KAAK,EAAE,CAAA;AACrB,CAAC;AAED,MAAM,UAAU,sBAAsB;IACpC,OAAO,WAAW,CAAC,IAAI,CAAA;AACzB,CAAC"}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
import type { Binary } from '@t4h.framework/core';
|
|
1
2
|
import { SftpClaim, type SftpAppendActivityInput, type SftpChmodActivityInput, type SftpCopyActivityInput, type SftpCwdActivityInput, type SftpDeleteActivityInput, type SftpDownloadDirActivityInput, type SftpExistsActivityInput, type SftpExistsResult, type SftpFileInfo, type SftpGetActivityInput, type SftpListActivityInput, type SftpMkdirActivityInput, type SftpPutActivityInput, type SftpRealPathActivityInput, type SftpRenameActivityInput, type SftpRmdirActivityInput, type SftpStat, type SftpStatActivityInput } from '@t4h.framework/sftp';
|
|
2
3
|
export declare class DevSftpClaim extends SftpClaim {
|
|
3
4
|
private readonly claims;
|
|
4
|
-
private
|
|
5
|
+
private stream;
|
|
5
6
|
append(input: SftpAppendActivityInput): Promise<void>;
|
|
6
7
|
chmod(input: SftpChmodActivityInput): Promise<void>;
|
|
7
8
|
cwd(input: SftpCwdActivityInput): Promise<string>;
|
|
8
9
|
delete(input: SftpDeleteActivityInput): Promise<void>;
|
|
9
|
-
downloadDir(input: SftpDownloadDirActivityInput): Promise<
|
|
10
|
+
downloadDir(input: SftpDownloadDirActivityInput): Promise<Binary>;
|
|
10
11
|
exists(input: SftpExistsActivityInput): Promise<SftpExistsResult>;
|
|
11
|
-
get(input: SftpGetActivityInput): Promise<
|
|
12
|
+
get(input: SftpGetActivityInput): Promise<Binary>;
|
|
12
13
|
list(input: SftpListActivityInput): Promise<SftpFileInfo[]>;
|
|
13
14
|
mkdir(input: SftpMkdirActivityInput): Promise<void>;
|
|
14
15
|
put(input: SftpPutActivityInput): Promise<void>;
|
|
@@ -1,117 +1,114 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { mkdir, mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { extname, join } from 'node:path';
|
|
5
6
|
import { Claim } from '@t4h.framework/core';
|
|
6
7
|
import { FileSystemClaim } from '@t4h.framework/fs';
|
|
7
|
-
import { SftpClaim, } from '@t4h.framework/sftp';
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
const url = new URL(connectionString);
|
|
12
|
-
if (url.protocol !== 'sftp:')
|
|
13
|
-
throw new Error(`Unsupported protocol: ${url.protocol}`);
|
|
14
|
-
const config = {
|
|
15
|
-
host: url.hostname,
|
|
16
|
-
port: url.port ? Number(url.port) : 22,
|
|
17
|
-
username: decodeURIComponent(url.username),
|
|
18
|
-
};
|
|
19
|
-
if (url.password) {
|
|
20
|
-
const password = decodeURIComponent(url.password);
|
|
21
|
-
if (password.startsWith('privateKey:'))
|
|
22
|
-
config.privateKey = password.slice(11);
|
|
23
|
-
else
|
|
24
|
-
config.password = password;
|
|
25
|
-
}
|
|
26
|
-
const passphrase = url.searchParams.get('passphrase');
|
|
27
|
-
if (passphrase)
|
|
28
|
-
config.passphrase = passphrase;
|
|
29
|
-
if (!config.password && !config.privateKey)
|
|
30
|
-
throw new Error('SFTP connection requires a password or private key');
|
|
31
|
-
return config;
|
|
32
|
-
}
|
|
8
|
+
import { SftpBinary, SftpClaim, } from '@t4h.framework/sftp';
|
|
9
|
+
import mime from 'mime-types';
|
|
10
|
+
import { create } from 'tar';
|
|
11
|
+
import { SftpClient } from '../SftpClient.js';
|
|
33
12
|
export class DevSftpClaim extends SftpClaim {
|
|
34
13
|
claims = new Claim({
|
|
35
14
|
fs: FileSystemClaim,
|
|
36
15
|
});
|
|
37
|
-
async
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return await fn(client);
|
|
42
|
-
}
|
|
43
|
-
finally {
|
|
44
|
-
await client.end().catch(() => { });
|
|
45
|
-
}
|
|
16
|
+
async stream(content) {
|
|
17
|
+
if (!(content instanceof SftpBinary))
|
|
18
|
+
return content.stream();
|
|
19
|
+
return SftpClient.stream(content.connectionString, content.filepath);
|
|
46
20
|
}
|
|
47
21
|
async append(input) {
|
|
48
|
-
await
|
|
49
|
-
|
|
50
|
-
await client.append(stream, input.filepath);
|
|
51
|
-
});
|
|
22
|
+
await using client = await SftpClient.connect(input.connectionString);
|
|
23
|
+
await client.append(await this.stream(input.content), input.filepath);
|
|
52
24
|
}
|
|
53
25
|
async chmod(input) {
|
|
54
|
-
await
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
26
|
+
await using client = await SftpClient.connect(input.connectionString);
|
|
27
|
+
const mode = (() => {
|
|
28
|
+
if (typeof input.mode === 'number')
|
|
29
|
+
return input.mode;
|
|
30
|
+
const mode = Number.parseInt(input.mode, 8);
|
|
31
|
+
if (Number.isNaN(mode))
|
|
32
|
+
throw new Error(`Invalid chmod mode "${input.mode}" (expected octal string or number)`);
|
|
33
|
+
return mode;
|
|
34
|
+
})();
|
|
35
|
+
await client.chmod(input.path, mode);
|
|
36
|
+
}
|
|
37
|
+
async cwd(input) {
|
|
38
|
+
await using client = await SftpClient.connect(input.connectionString);
|
|
39
|
+
return await client.cwd();
|
|
58
40
|
}
|
|
59
41
|
async delete(input) {
|
|
60
|
-
await
|
|
42
|
+
await using client = await SftpClient.connect(input.connectionString);
|
|
43
|
+
await client.delete(input.path, input.noErrorOK ?? false);
|
|
61
44
|
}
|
|
62
45
|
async downloadDir(input) {
|
|
63
|
-
const
|
|
64
|
-
const
|
|
46
|
+
const workDir = await mkdtemp(join(tmpdir(), 'sftp-dl-'));
|
|
47
|
+
const contentDir = join(workDir, 'content');
|
|
48
|
+
const archivePath = join(workDir, 'archive.tar.gz');
|
|
65
49
|
try {
|
|
66
|
-
await
|
|
67
|
-
await
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
await fs.
|
|
72
|
-
throw error;
|
|
50
|
+
await using client = await SftpClient.connect(input.connectionString);
|
|
51
|
+
await mkdir(contentDir, { recursive: true });
|
|
52
|
+
await client.downloadDir(input.path, contentDir);
|
|
53
|
+
await create({ gzip: true, file: archivePath, cwd: contentDir }, ['.']);
|
|
54
|
+
await rm(contentDir, { recursive: true, force: true });
|
|
55
|
+
return await this.claims.fs.write(`${randomUUID()}-sftp-download-dir.tar.gz`, createReadStream(archivePath));
|
|
73
56
|
}
|
|
74
57
|
finally {
|
|
75
|
-
await
|
|
58
|
+
await rm(workDir, { recursive: true, force: true });
|
|
76
59
|
}
|
|
77
60
|
}
|
|
78
|
-
exists(input) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
61
|
+
async exists(input) {
|
|
62
|
+
await using client = await SftpClient.connect(input.connectionString);
|
|
63
|
+
return await client.exists(input.path);
|
|
64
|
+
}
|
|
65
|
+
async get(input) {
|
|
66
|
+
await using client = await SftpClient.connect(input.connectionString);
|
|
67
|
+
const stat = await client.stat(input.filepath);
|
|
68
|
+
const contentType = mime.lookup(extname(input.filepath)) || 'application/octet-stream';
|
|
69
|
+
return SftpBinary.from(input.connectionString, {
|
|
70
|
+
filepath: input.filepath,
|
|
71
|
+
contentType,
|
|
72
|
+
contentLength: stat.size,
|
|
73
|
+
}, binary => SftpClient.stream(binary.connectionString, binary.filepath));
|
|
74
|
+
}
|
|
75
|
+
async list(input) {
|
|
76
|
+
await using client = await SftpClient.connect(input.connectionString);
|
|
77
|
+
const entries = await client.list(input.path);
|
|
78
|
+
return entries.map(entry => {
|
|
79
|
+
const types = ['d', '-', 'l'];
|
|
80
|
+
if (!types.includes(entry.type))
|
|
81
|
+
throw new Error(`Unexpected SFTP list entry type "${entry.type}" for "${entry.name}"`);
|
|
82
|
+
return entry;
|
|
87
83
|
});
|
|
88
84
|
}
|
|
89
|
-
list(input) {
|
|
90
|
-
return this.withClient(input.connectionString, client => client.list(input.path));
|
|
91
|
-
}
|
|
92
85
|
async mkdir(input) {
|
|
93
|
-
await
|
|
86
|
+
await using client = await SftpClient.connect(input.connectionString);
|
|
87
|
+
await client.mkdir(input.path, input.recursive ?? false);
|
|
94
88
|
}
|
|
95
89
|
async put(input) {
|
|
96
|
-
await
|
|
97
|
-
|
|
98
|
-
await client.put(stream, input.filepath);
|
|
99
|
-
});
|
|
90
|
+
await using client = await SftpClient.connect(input.connectionString);
|
|
91
|
+
await client.put(await this.stream(input.content), input.filepath);
|
|
100
92
|
}
|
|
101
93
|
async copy(input) {
|
|
102
|
-
await
|
|
94
|
+
await using client = await SftpClient.connect(input.connectionString);
|
|
95
|
+
await client.rcopy(input.srcPath, input.destPath);
|
|
103
96
|
}
|
|
104
|
-
realPath(input) {
|
|
105
|
-
|
|
97
|
+
async realPath(input) {
|
|
98
|
+
await using client = await SftpClient.connect(input.connectionString);
|
|
99
|
+
return await client.realPath(input.path);
|
|
106
100
|
}
|
|
107
101
|
async rename(input) {
|
|
108
|
-
await
|
|
102
|
+
await using client = await SftpClient.connect(input.connectionString);
|
|
103
|
+
await client.rename(input.srcPath, input.destPath);
|
|
109
104
|
}
|
|
110
105
|
async rmdir(input) {
|
|
111
|
-
await
|
|
106
|
+
await using client = await SftpClient.connect(input.connectionString);
|
|
107
|
+
await client.rmdir(input.path, input.recursive ?? false);
|
|
112
108
|
}
|
|
113
|
-
stat(input) {
|
|
114
|
-
|
|
109
|
+
async stat(input) {
|
|
110
|
+
await using client = await SftpClient.connect(input.connectionString);
|
|
111
|
+
return (await client.stat(input.path));
|
|
115
112
|
}
|
|
116
113
|
}
|
|
117
114
|
//# sourceMappingURL=DevSftpClaim.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DevSftpClaim.js","sourceRoot":"","sources":["../../../src/models/claims/DevSftpClaim.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"DevSftpClaim.js","sourceRoot":"","sources":["../../../src/models/claims/DevSftpClaim.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export async function getSftpConfigFromConnectionString(connectionString) {
|
|
2
|
+
const url = new URL(connectionString);
|
|
3
|
+
if (url.protocol !== 'sftp:')
|
|
4
|
+
throw new Error(`Unsupported protocol: ${url.protocol}`);
|
|
5
|
+
const config = {
|
|
6
|
+
host: url.hostname,
|
|
7
|
+
port: url.port ? Number(url.port) : 22,
|
|
8
|
+
};
|
|
9
|
+
if (url.username)
|
|
10
|
+
config.username = decodeURIComponent(url.username);
|
|
11
|
+
if (url.password) {
|
|
12
|
+
const password = decodeURIComponent(url.password);
|
|
13
|
+
if (password.startsWith('privateKey:'))
|
|
14
|
+
config.privateKey = password.slice(11);
|
|
15
|
+
else
|
|
16
|
+
config.password = password;
|
|
17
|
+
}
|
|
18
|
+
const passphrase = url.searchParams.get('passphrase');
|
|
19
|
+
if (passphrase)
|
|
20
|
+
config.passphrase = passphrase;
|
|
21
|
+
if (!config.password && !config.privateKey)
|
|
22
|
+
throw new Error('SFTP connection requires a password in the URL or a private key');
|
|
23
|
+
return config;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=get-sftp-config-from-connection-string.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-sftp-config-from-connection-string.js","sourceRoot":"","sources":["../../src/tools/get-sftp-config-from-connection-string.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,KAAK,UAAU,iCAAiC,CACrD,gBAAwB;IAExB,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,gBAAgB,CAAC,CAAA;IAErC,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO;QAC1B,MAAM,IAAI,KAAK,CAAC,yBAAyB,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAA;IAE1D,MAAM,MAAM,GAA8B;QACxC,IAAI,EAAE,GAAG,CAAC,QAAQ;QAClB,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE;KACvC,CAAA;IAED,IAAI,GAAG,CAAC,QAAQ;QAAE,MAAM,CAAC,QAAQ,GAAG,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IAEpE,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC;QACjB,MAAM,QAAQ,GAAG,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QAEjD,IAAI,QAAQ,CAAC,UAAU,CAAC,aAAa,CAAC;YACpC,MAAM,CAAC,UAAU,GAAG,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;;YACnC,MAAM,CAAC,QAAQ,GAAG,QAAQ,CAAA;IACjC,CAAC;IAED,MAAM,UAAU,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;IAErD,IAAI,UAAU;QAAE,MAAM,CAAC,UAAU,GAAG,UAAU,CAAA;IAE9C,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,CAAC,MAAM,CAAC,UAAU;QACxC,MAAM,IAAI,KAAK,CACb,iEAAiE,CAClE,CAAA;IAEH,OAAO,MAAM,CAAA;AACf,CAAC"}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { Binary } from '@t4h.framework/core';
|
|
2
|
+
import { SftpBinary } from '@t4h.framework/sftp';
|
|
2
3
|
import { JSONCodec, JSONCodecKind } from '../models/JSONCodec.js';
|
|
3
4
|
import { JSONCodecKindFrameworkSerializableClass as Kind } from '../models/JSONCodecKindFrameworkSerializableClass.js';
|
|
4
5
|
export declare const jsonCodec: JSONCodec<[JSONCodecKind<"date", string, Date, {}, {}>, JSONCodecKind<"binary", {
|
|
5
6
|
path: string;
|
|
6
7
|
contentType: string;
|
|
7
8
|
contentLength: number;
|
|
8
|
-
}, Binary, {}, {}>,
|
|
9
|
+
}, Binary, {}, {}>, JSONCodecKind<"sftp:binary", {
|
|
10
|
+
connectionString: string;
|
|
11
|
+
file: {
|
|
12
|
+
filepath: string;
|
|
13
|
+
contentType: string;
|
|
14
|
+
contentLength: number;
|
|
15
|
+
};
|
|
16
|
+
}, SftpBinary, {}, {}>, Kind<"orm", import("@t4h.framework/core").SerializableClass>, Kind<"http:body", import("@t4h.framework/core").SerializableClass>, Kind<"oauth2:client-credentials-grant", import("@t4h.framework/core").SerializableClass>, Kind<"oauth2:grant-resource-owner-password", import("@t4h.framework/core").SerializableClass>]>;
|
package/dist/tools/json-codec.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { Binary } from '@t4h.framework/core';
|
|
2
2
|
import { Body, OAuth2ClientCredentials, OAuth2GrantResourceOwnerPassword, } from '@t4h.framework/http';
|
|
3
3
|
import { ORM } from '@t4h.framework/orm';
|
|
4
|
+
import { SftpBinary } from '@t4h.framework/sftp';
|
|
4
5
|
import { DevFileBinary } from '../models/DevFileBinary.js';
|
|
5
6
|
import { JSONCodec, JSONCodecKind } from '../models/JSONCodec.js';
|
|
6
7
|
import { JSONCodecKindFrameworkSerializableClass as Kind } from '../models/JSONCodecKindFrameworkSerializableClass.js';
|
|
8
|
+
import { SftpClient } from '../models/SftpClient.js';
|
|
7
9
|
const jsonDateKind = new JSONCodecKind('date', value => value instanceof Date, (date) => date.toISOString(), (date) => new Date(date));
|
|
8
|
-
const
|
|
10
|
+
const jsonFileBinaryKind = new JSONCodecKind('binary', value => value instanceof DevFileBinary, (binary) => {
|
|
9
11
|
if (!(binary instanceof DevFileBinary))
|
|
10
12
|
throw new TypeError('Unsupported binary instance');
|
|
11
13
|
return binary.toJSON();
|
|
12
14
|
}, json => DevFileBinary.from(json));
|
|
13
|
-
|
|
15
|
+
const jsonSftpBinaryKind = new JSONCodecKind('sftp:binary', value => value instanceof SftpBinary, (binary) => {
|
|
16
|
+
if (!(binary instanceof SftpBinary))
|
|
17
|
+
throw new TypeError('Unsupported binary instance');
|
|
18
|
+
return binary.toJSON();
|
|
19
|
+
}, json => SftpBinary.from(json.connectionString, json.file, binary => SftpClient.stream(binary.connectionString, binary.filepath)));
|
|
20
|
+
export const jsonCodec = new JSONCodec(jsonDateKind, jsonFileBinaryKind, jsonSftpBinaryKind, new Kind('orm', ORM), new Kind('http:body', Body), new Kind('oauth2:client-credentials-grant', OAuth2ClientCredentials), new Kind('oauth2:grant-resource-owner-password', OAuth2GrantResourceOwnerPassword));
|
|
14
21
|
//# sourceMappingURL=json-codec.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json-codec.js","sourceRoot":"","sources":["../../src/tools/json-codec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAC5C,OAAO,EACL,IAAI,EACJ,uBAAuB,EACvB,gCAAgC,GACjC,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"json-codec.js","sourceRoot":"","sources":["../../src/tools/json-codec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAC5C,OAAO,EACL,IAAI,EACJ,uBAAuB,EACvB,gCAAgC,GACjC,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAA;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAEhD,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAA;AAC1D,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AACjE,OAAO,EAAE,uCAAuC,IAAI,IAAI,EAAE,MAAM,sDAAsD,CAAA;AACtH,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AAEpD,MAAM,YAAY,GAAG,IAAI,aAAa,CACpC,MAAM,EACN,KAAK,CAAC,EAAE,CAAC,KAAK,YAAY,IAAI,EAC9B,CAAC,IAAU,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,EAClC,CAAC,IAAY,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,CACjC,CAAA;AAED,MAAM,kBAAkB,GAAG,IAAI,aAAa,CAC1C,QAAQ,EACR,KAAK,CAAC,EAAE,CAAC,KAAK,YAAY,aAAa,EACvC,CAAC,MAAc,EAAE,EAAE;IACjB,IAAI,CAAC,CAAC,MAAM,YAAY,aAAa,CAAC;QACpC,MAAM,IAAI,SAAS,CAAC,6BAA6B,CAAC,CAAA;IAEpD,OAAO,MAAM,CAAC,MAAM,EAAE,CAAA;AACxB,CAAC,EACD,IAAI,CAAC,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CACjC,CAAA;AAED,MAAM,kBAAkB,GAAG,IAAI,aAAa,CAC1C,aAAa,EACb,KAAK,CAAC,EAAE,CAAC,KAAK,YAAY,UAAU,EACpC,CAAC,MAAkB,EAAE,EAAE;IACrB,IAAI,CAAC,CAAC,MAAM,YAAY,UAAU,CAAC;QACjC,MAAM,IAAI,SAAS,CAAC,6BAA6B,CAAC,CAAA;IAEpD,OAAO,MAAM,CAAC,MAAM,EAAE,CAAA;AACxB,CAAC,EACD,IAAI,CAAC,EAAE,CACL,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CACzD,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAAC,QAAQ,CAAC,CAC5D,CACJ,CAAA;AAED,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,SAAS,CACpC,YAAY,EACZ,kBAAkB,EAClB,kBAAkB,EAClB,IAAI,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,EACpB,IAAI,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,EAC3B,IAAI,IAAI,CAAC,iCAAiC,EAAE,uBAAuB,CAAC,EACpE,IAAI,IAAI,CACN,sCAAsC,EACtC,gCAAgC,CACjC,CACF,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@t4h.framework/dev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Dev server for the T4H Framework",
|
|
5
5
|
"homepage": "https://github.com/tech4humans-brasil/framework/tree/main/packages/dev",
|
|
6
6
|
"bugs": "https://github.com/tech4humans-brasil/framework/issues",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"@t4h.framework/http": "^0.3.0",
|
|
43
43
|
"@t4h.framework/orm": "^0.4.0",
|
|
44
44
|
"@t4h.framework/runtime-claims": "^0.1.0",
|
|
45
|
-
"@t4h.framework/sftp": "^0.
|
|
45
|
+
"@t4h.framework/sftp": "^0.5.0",
|
|
46
46
|
"@t4h.framework/smtp": "^0.4.0",
|
|
47
47
|
"mime-types": "^3.0.2",
|
|
48
48
|
"nodemailer": "^8.0.1",
|