1+ import { execSync , spawnSync } from "node:child_process" ;
2+ import path from "node:path" ;
3+
4+ export type RcloneDestinationType =
5+ | "s3"
6+ | "ftp"
7+ | "sftp"
8+ | "google-drive"
9+ | "onedrive"
10+ | "dropbox"
11+ | "b2"
12+ | "azure" ;
13+
14+ export interface RcloneS3Config {
15+ type : "s3" ;
16+ accessKeyId : string ;
17+ secretAccessKey : string ;
18+ region : string ;
19+ bucket : string ;
20+ endpoint ?: string ;
21+ }
22+
23+ export interface RcloneFtpConfig {
24+ type : "ftp" ;
25+ host : string ;
26+ port ?: number ;
27+ user : string ;
28+ password : string ;
29+ remotePath : string ;
30+ }
31+
32+ export interface RcloneSftpConfig {
33+ type : "sftp" ;
34+ host : string ;
35+ port ?: number ;
36+ user : string ;
37+ password ?: string ;
38+ privateKey ?: string ;
39+ remotePath : string ;
40+ }
41+
42+ export interface RcloneGoogleDriveConfig {
43+ type : "google-drive" ;
44+ clientId : string ;
45+ clientSecret : string ;
46+ token : string ;
47+ rootFolderId ?: string ;
48+ remotePath : string ;
49+ }
50+
51+ export interface RcloneOnedriveConfig {
52+ type : "onedrive" ;
53+ clientId : string ;
54+ clientSecret : string ;
55+ token : string ;
56+ driveId ?: string ;
57+ remotePath : string ;
58+ }
59+
60+ export interface RcloneDropboxConfig {
61+ type : "dropbox" ;
62+ clientId : string ;
63+ clientSecret : string ;
64+ token : string ;
65+ remotePath : string ;
66+ }
67+
68+ export interface RcloneB2Config {
69+ type : "b2" ;
70+ accountId : string ;
71+ accountKey : string ;
72+ bucket : string ;
73+ remotePath ?: string ;
74+ }
75+
76+ export interface RcloneAzureConfig {
77+ type : "azure" ;
78+ account : string ;
79+ key : string ;
80+ container : string ;
81+ remotePath ?: string ;
82+ }
83+
84+ export type RcloneConfig =
85+ | RcloneS3Config
86+ | RcloneFtpConfig
87+ | RcloneSftpConfig
88+ | RcloneGoogleDriveConfig
89+ | RcloneOnedriveConfig
90+ | RcloneDropboxConfig
91+ | RcloneB2Config
92+ | RcloneAzureConfig ;
93+
94+ /**
95+ * Build rclone config file content and remote path for a given destination config.
96+ */
97+ export function buildRcloneConfig ( config : RcloneConfig ) : {
98+ configContent : string ;
99+ remotePath : string ;
100+ } {
101+ const remoteName = "dokploy-remote" ;
102+
103+ switch ( config . type ) {
104+ case "s3" : {
105+ const configContent = [
106+ `[${ remoteName } ]` ,
107+ "type = s3" ,
108+ "provider = AWS" ,
109+ `access_key_id = ${ config . accessKeyId } ` ,
110+ `secret_access_key = ${ config . secretAccessKey } ` ,
111+ `region = ${ config . region } ` ,
112+ config . endpoint ? `endpoint = ${ config . endpoint } ` : "" ,
113+ ]
114+ . filter ( Boolean )
115+ . join ( "\n" ) ;
116+ return {
117+ configContent,
118+ remotePath : `${ remoteName } :${ config . bucket } ` ,
119+ } ;
120+ }
121+
122+ case "ftp" : {
123+ const configContent = [
124+ `[${ remoteName } ]` ,
125+ "type = ftp" ,
126+ `host = ${ config . host } ` ,
127+ `port = ${ config . port ?? 21 } ` ,
128+ `user = ${ config . user } ` ,
129+ `pass = ${ config . password } ` ,
130+ ] . join ( "\n" ) ;
131+ return {
132+ configContent,
133+ remotePath : `${ remoteName } :${ config . remotePath } ` ,
134+ } ;
135+ }
136+
137+ case "sftp" : {
138+ const lines = [
139+ `[${ remoteName } ]` ,
140+ "type = sftp" ,
141+ `host = ${ config . host } ` ,
142+ `port = ${ config . port ?? 22 } ` ,
143+ `user = ${ config . user } ` ,
144+ ] ;
145+ if ( config . password ) {
146+ lines . push ( `pass = ${ config . password } ` ) ;
147+ }
148+ if ( config . privateKey ) {
149+ lines . push ( `key_pem = ${ config . privateKey } ` ) ;
150+ }
151+ return {
152+ configContent : lines . join ( "\n" ) ,
153+ remotePath : `${ remoteName } :${ config . remotePath } ` ,
154+ } ;
155+ }
156+
157+ case "google-drive" : {
158+ const configContent = [
159+ `[${ remoteName } ]` ,
160+ "type = drive" ,
161+ `client_id = ${ config . clientId } ` ,
162+ `client_secret = ${ config . clientSecret } ` ,
163+ `token = ${ config . token } ` ,
164+ config . rootFolderId
165+ ? `root_folder_id = ${ config . rootFolderId } `
166+ : "" ,
167+ ]
168+ . filter ( Boolean )
169+ . join ( "\n" ) ;
170+ return {
171+ configContent,
172+ remotePath : `${ remoteName } :${ config . remotePath } ` ,
173+ } ;
174+ }
175+
176+ case "onedrive" : {
177+ const configContent = [
178+ `[${ remoteName } ]` ,
179+ "type = onedrive" ,
180+ `client_id = ${ config . clientId } ` ,
181+ `client_secret = ${ config . clientSecret } ` ,
182+ `token = ${ config . token } ` ,
183+ config . driveId ? `drive_id = ${ config . driveId } ` : "" ,
184+ ]
185+ . filter ( Boolean )
186+ . join ( "\n" ) ;
187+ return {
188+ configContent,
189+ remotePath : `${ remoteName } :${ config . remotePath } ` ,
190+ } ;
191+ }
192+
193+ case "dropbox" : {
194+ const configContent = [
195+ `[${ remoteName } ]` ,
196+ "type = dropbox" ,
197+ `client_id = ${ config . clientId } ` ,
198+ `client_secret = ${ config . clientSecret } ` ,
199+ `token = ${ config . token } ` ,
200+ ] . join ( "\n" ) ;
201+ return {
202+ configContent,
203+ remotePath : `${ remoteName } :${ config . remotePath } ` ,
204+ } ;
205+ }
206+
207+ case "b2" : {
208+ const configContent = [
209+ `[${ remoteName } ]` ,
210+ "type = b2" ,
211+ `account = ${ config . accountId } ` ,
212+ `key = ${ config . accountKey } ` ,
213+ ] . join ( "\n" ) ;
214+ return {
215+ configContent,
216+ remotePath : `${ remoteName } :${ config . bucket } ${ config . remotePath ? `/${ config . remotePath } ` : "" } ` ,
217+ } ;
218+ }
219+
220+ case "azure" : {
221+ const configContent = [
222+ `[${ remoteName } ]` ,
223+ "type = azureblob" ,
224+ `account = ${ config . account } ` ,
225+ `key = ${ config . key } ` ,
226+ ] . join ( "\n" ) ;
227+ return {
228+ configContent,
229+ remotePath : `${ remoteName } :${ config . container } ${ config . remotePath ? `/${ config . remotePath } ` : "" } ` ,
230+ } ;
231+ }
232+
233+ default : {
234+ throw new Error ( `Unsupported rclone destination type` ) ;
235+ }
236+ }
237+ }
238+
239+ /**
240+ * Upload a local file to a remote destination using rclone.
241+ *
242+ * @param localFilePath - Absolute path to the local file to upload.
243+ * @param config - Rclone destination configuration.
244+ * @param remoteFileName - Optional filename to use on the remote. Defaults to the local filename.
245+ */
246+ export async function uploadFileWithRclone (
247+ localFilePath : string ,
248+ config : RcloneConfig ,
249+ remoteFileName ?: string ,
250+ ) : Promise < void > {
251+ const { configContent, remotePath } = buildRcloneConfig ( config ) ;
252+
253+ // Write config to a temp file
254+ const os = await import ( "node:os" ) ;
255+ const fs = await import ( "node:fs" ) ;
256+ const tmpDir = os . tmpdir ( ) ;
257+ const configFilePath = path . join (
258+ tmpDir ,
259+ `rclone-${ Date . now ( ) } -${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } .conf` ,
260+ ) ;
261+
262+ try {
263+ fs . writeFileSync ( configFilePath , configContent , { mode : 0o600 } ) ;
264+
265+ const destFileName =
266+ remoteFileName ?? path . basename ( localFilePath ) ;
267+ const fullRemotePath = `${ remotePath } /${ destFileName } ` ;
268+
269+ const result = spawnSync (
270+ "rclone" ,
271+ [
272+ "copyto" ,
273+ localFilePath ,
274+ fullRemotePath ,
275+ "--config" ,
276+ configFilePath ,
277+ "--no-traverse" ,
278+ ] ,
279+ { encoding : "utf-8" } ,
280+ ) ;
281+
282+ if ( result . status !== 0 ) {
283+ throw new Error (
284+ `rclone upload failed: ${ result . stderr || result . stdout || "unknown error" } ` ,
285+ ) ;
286+ }
287+ } finally {
288+ try {
289+ const fs = await import ( "node:fs" ) ;
290+ fs . unlinkSync ( configFilePath ) ;
291+ } catch {
292+ // ignore cleanup errors
293+ }
294+ }
295+ }
296+
297+ /**
298+ * Check whether rclone is available in PATH.
299+ */
300+ export function isRcloneAvailable ( ) : boolean {
301+ try {
302+ execSync ( "rclone version" , { stdio : "ignore" } ) ;
303+ return true ;
304+ } catch {
305+ return false ;
306+ }
307+ }
0 commit comments