1
1
/*
2
- A BTRFS Filesystem
2
+ BTRFS Filesystem
3
3
4
4
DEVELOPMENT:
5
5
6
6
Start node, then:
7
7
8
8
DEBUG="cocalc:*file-server*" DEBUG_CONSOLE=yes node
9
9
10
- a = require('@cocalc/file-server/storage- btrfs'); fs = await a.filesystem({device:'/tmp/btrfs.img', formatIfNeeded:true, mount:'/mnt/btrfs', uid:293597964})
10
+ a = require('@cocalc/file-server/btrfs'); fs = await a.filesystem({device:'/tmp/btrfs.img', formatIfNeeded:true, mount:'/mnt/btrfs', uid:293597964})
11
11
12
12
*/
13
13
14
14
import refCache from "@cocalc/util/refcache" ;
15
- import { exists , isdir , listdir , mkdirp , rmdir , sudo } from "./util" ;
16
- import { subvolume , type Subvolume } from "./subvolume" ;
17
- import { SNAPSHOTS } from "./subvolume-snapshots" ;
18
- import { join , normalize } from "path" ;
15
+ import { exists , mkdirp , btrfs , sudo } from "./util" ;
16
+ import { join } from "path" ;
17
+ import { Subvolumes } from "./subvolumes" ;
19
18
20
19
// default size of btrfs filesystem if creating an image file.
21
20
const DEFAULT_FILESYSTEM_SIZE = "10G" ;
@@ -25,8 +24,6 @@ export const DEFAULT_SUBVOLUME_SIZE = "1G";
25
24
26
25
const MOUNT_ERROR = "wrong fs type, bad option, bad superblock" ;
27
26
28
- const RESERVED = new Set ( [ "bup" , "recv" , "streams" , SNAPSHOTS ] ) ;
29
-
30
27
export interface Options {
31
28
// the underlying block device.
32
29
// If this is a file (or filename) ending in .img, then it's a sparse file mounted as a loopback device.
@@ -40,9 +37,6 @@ export interface Options {
40
37
// where the btrfs filesystem is mounted
41
38
mount : string ;
42
39
43
- // all subvolumes will have this owner
44
- uid ?: number ;
45
-
46
40
// default size of newly created subvolumes
47
41
defaultSize ?: string | number ;
48
42
defaultFilesystemSize ?: string | number ;
@@ -52,6 +46,7 @@ export class Filesystem {
52
46
public readonly opts : Options ;
53
47
public readonly bup : string ;
54
48
public readonly streams : string ;
49
+ public readonly subvolumes : Subvolumes ;
55
50
56
51
constructor ( opts : Options ) {
57
52
opts = {
@@ -62,6 +57,7 @@ export class Filesystem {
62
57
this . opts = opts ;
63
58
this . bup = join ( this . opts . mount , "bup" ) ;
64
59
this . streams = join ( this . opts . mount , "streams" ) ;
60
+ this . subvolumes = new Subvolumes ( this ) ;
65
61
}
66
62
67
63
init = async ( ) => {
@@ -71,8 +67,7 @@ export class Filesystem {
71
67
await this . initDevice ( ) ;
72
68
await this . mountFilesystem ( ) ;
73
69
await sudo ( { command : "chmod" , args : [ "a+rx" , this . opts . mount ] } ) ;
74
- await sudo ( {
75
- command : "btrfs" ,
70
+ await btrfs ( {
76
71
args : [ "quota" , "enable" , "--simple" , this . opts . mount ] ,
77
72
} ) ;
78
73
await sudo ( {
@@ -95,8 +90,7 @@ export class Filesystem {
95
90
} ;
96
91
97
92
info = async ( ) : Promise < { [ field : string ] : string } > => {
98
- const { stdout } = await sudo ( {
99
- command : "btrfs" ,
93
+ const { stdout } = await btrfs ( {
100
94
args : [ "subvolume" , "show" , this . opts . mount ] ,
101
95
} ) ;
102
96
const obj : { [ field : string ] : string } = { } ;
@@ -108,7 +102,6 @@ export class Filesystem {
108
102
return obj ;
109
103
} ;
110
104
111
- //
112
105
private mountFilesystem = async ( ) => {
113
106
try {
114
107
await this . info ( ) ;
@@ -148,11 +141,25 @@ export class Filesystem {
148
141
"btrfs" ,
149
142
this . opts . mount ,
150
143
) ;
151
- return await sudo ( {
152
- command : "mount" ,
153
- args,
144
+ {
145
+ const { stderr, exit_code } = await sudo ( {
146
+ command : "mount" ,
147
+ args,
148
+ err_on_exit : false ,
149
+ } ) ;
150
+ if ( exit_code ) {
151
+ return { stderr, exit_code } ;
152
+ }
153
+ }
154
+ const { stderr, exit_code } = await sudo ( {
155
+ command : "chown" ,
156
+ args : [
157
+ `${ process . getuid ?.( ) ?? 0 } :${ process . getgid ?.( ) ?? 0 } ` ,
158
+ this . opts . mount ,
159
+ ] ,
154
160
err_on_exit : false ,
155
161
} ) ;
162
+ return { stderr, exit_code } ;
156
163
} ;
157
164
158
165
unmount = async ( ) => {
@@ -167,108 +174,7 @@ export class Filesystem {
167
174
await sudo ( { command : "mkfs.btrfs" , args : [ this . opts . device ] } ) ;
168
175
} ;
169
176
170
- close = ( ) => {
171
- // nothing, yet
172
- } ;
173
-
174
- subvolume = async ( name : string ) : Promise < Subvolume > => {
175
- if ( RESERVED . has ( name ) ) {
176
- throw Error ( `${ name } is reserved` ) ;
177
- }
178
- return await subvolume ( { filesystem : this , name } ) ;
179
- } ;
180
-
181
- // create a subvolume by cloning an existing one.
182
- cloneSubvolume = async ( source : string , name : string ) => {
183
- if ( RESERVED . has ( name ) ) {
184
- throw Error ( `${ name } is reserved` ) ;
185
- }
186
- if ( ! ( await exists ( join ( this . opts . mount , source ) ) ) ) {
187
- throw Error ( `subvolume ${ source } does not exist` ) ;
188
- }
189
- if ( await exists ( join ( this . opts . mount , name ) ) ) {
190
- throw Error ( `subvolume ${ name } already exists` ) ;
191
- }
192
- await sudo ( {
193
- command : "btrfs" ,
194
- args : [
195
- "subvolume" ,
196
- "snapshot" ,
197
- join ( this . opts . mount , source ) ,
198
- join ( this . opts . mount , source , name ) ,
199
- ] ,
200
- } ) ;
201
- await sudo ( {
202
- command : "mv" ,
203
- args : [ join ( this . opts . mount , source , name ) , join ( this . opts . mount , name ) ] ,
204
- } ) ;
205
- const snapdir = join ( this . opts . mount , name , SNAPSHOTS ) ;
206
- if ( await exists ( snapdir ) ) {
207
- const snapshots = await listdir ( snapdir ) ;
208
- await rmdir (
209
- snapshots . map ( ( x ) => join ( this . opts . mount , name , SNAPSHOTS , x ) ) ,
210
- ) ;
211
- }
212
- const src = await this . subvolume ( source ) ;
213
- const vol = await this . subvolume ( name ) ;
214
- const { size } = await src . quota . get ( ) ;
215
- if ( size ) {
216
- await vol . quota . set ( size ) ;
217
- }
218
- return vol ;
219
- } ;
220
-
221
- deleteSubvolume = async ( name : string ) => {
222
- await sudo ( {
223
- command : "btrfs" ,
224
- args : [ "subvolume" , "delete" , join ( this . opts . mount , name ) ] ,
225
- } ) ;
226
- } ;
227
-
228
- list = async ( ) : Promise < string [ ] > => {
229
- const { stdout } = await sudo ( {
230
- command : "btrfs" ,
231
- args : [ "subvolume" , "list" , this . opts . mount ] ,
232
- } ) ;
233
- return stdout
234
- . split ( "\n" )
235
- . map ( ( x ) => x . split ( " " ) . slice ( - 1 ) [ 0 ] )
236
- . filter ( ( x ) => x )
237
- . sort ( ) ;
238
- } ;
239
-
240
- rsync = async ( {
241
- src,
242
- target,
243
- args = [ "-axH" ] ,
244
- timeout = 5 * 60 * 1000 ,
245
- } : {
246
- src : string ;
247
- target : string ;
248
- args ?: string [ ] ;
249
- timeout ?: number ;
250
- } ) : Promise < { stdout : string ; stderr : string ; exit_code : number } > => {
251
- let srcPath = normalize ( join ( this . opts . mount , src ) ) ;
252
- if ( ! srcPath . startsWith ( this . opts . mount ) ) {
253
- throw Error ( "suspicious source" ) ;
254
- }
255
- let targetPath = normalize ( join ( this . opts . mount , target ) ) ;
256
- if ( ! targetPath . startsWith ( this . opts . mount ) ) {
257
- throw Error ( "suspicious target" ) ;
258
- }
259
- if ( ! srcPath . endsWith ( "/" ) && ( await isdir ( srcPath ) ) ) {
260
- srcPath += "/" ;
261
- if ( ! targetPath . endsWith ( "/" ) ) {
262
- targetPath += "/" ;
263
- }
264
- }
265
- return await sudo ( {
266
- command : "rsync" ,
267
- args : [ ...args , srcPath , targetPath ] ,
268
- err_on_exit : false ,
269
- timeout : timeout / 1000 ,
270
- } ) ;
271
- } ;
177
+ close = ( ) => { } ;
272
178
}
273
179
274
180
function isImageFile ( name : string ) {
0 commit comments