@@ -4,16 +4,14 @@ A subvolume
4
4
5
5
import { type Filesystem , DEFAULT_SUBVOLUME_SIZE } from "./filesystem" ;
6
6
import refCache from "@cocalc/util/refcache" ;
7
- import { exists , listdir , mkdirp , sudo } from "./util" ;
7
+ import { exists , sudo } from "./util" ;
8
8
import { join , normalize } from "path" ;
9
9
import { SubvolumeFilesystem } from "./subvolume-fs" ;
10
10
import { SubvolumeBup } from "./subvolume-bup" ;
11
11
import { SubvolumeSnapshots } from "./subvolume-snapshots" ;
12
+ import { SubvolumeQuota } from "./subvolume-quota" ;
12
13
import getLogger from "@cocalc/backend/logger" ;
13
14
14
- const SEND_SNAPSHOT_PREFIX = "send-" ;
15
- const PAD = 4 ;
16
-
17
15
const logger = getLogger ( "file-server:storage-btrfs:subvolume" ) ;
18
16
19
17
interface Options {
@@ -29,6 +27,7 @@ export class Subvolume {
29
27
public readonly fs : SubvolumeFilesystem ;
30
28
public readonly bup : SubvolumeBup ;
31
29
public readonly snapshots : SubvolumeSnapshots ;
30
+ public readonly quota : SubvolumeQuota ;
32
31
33
32
constructor ( { filesystem, name } : Options ) {
34
33
this . filesystem = filesystem ;
@@ -37,6 +36,7 @@ export class Subvolume {
37
36
this . fs = new SubvolumeFilesystem ( this ) ;
38
37
this . bup = new SubvolumeBup ( this ) ;
39
38
this . snapshots = new SubvolumeSnapshots ( this ) ;
39
+ this . quota = new SubvolumeQuota ( this ) ;
40
40
}
41
41
42
42
init = async ( ) => {
@@ -47,7 +47,7 @@ export class Subvolume {
47
47
args : [ "subvolume" , "create" , this . path ] ,
48
48
} ) ;
49
49
await this . chown ( this . path ) ;
50
- await this . size (
50
+ await this . quota . set (
51
51
this . filesystem . opts . defaultSize ?? DEFAULT_SUBVOLUME_SIZE ,
52
52
) ;
53
53
}
@@ -80,163 +80,6 @@ export class Subvolume {
80
80
normalize = ( path : string ) => {
81
81
return join ( this . path , normalize ( path ) ) ;
82
82
} ;
83
-
84
- /////////////
85
- // QUOTA
86
- /////////////
87
-
88
- private quotaInfo = async ( ) => {
89
- const { stdout } = await sudo ( {
90
- verbose : false ,
91
- command : "btrfs" ,
92
- args : [ "--format=json" , "qgroup" , "show" , "-reF" , this . path ] ,
93
- } ) ;
94
- const x = JSON . parse ( stdout ) ;
95
- return x [ "qgroup-show" ] [ 0 ] ;
96
- } ;
97
-
98
- quota = async ( ) : Promise < {
99
- size : number ;
100
- used : number ;
101
- } > => {
102
- let { max_referenced : size , referenced : used } = await this . quotaInfo ( ) ;
103
- if ( size == "none" ) {
104
- size = null ;
105
- }
106
- return {
107
- used,
108
- size,
109
- } ;
110
- } ;
111
-
112
- size = async ( size : string | number ) => {
113
- if ( ! size ) {
114
- throw Error ( "size must be specified" ) ;
115
- }
116
- await sudo ( {
117
- command : "btrfs" ,
118
- args : [ "qgroup" , "limit" , `${ size } ` , this . path ] ,
119
- } ) ;
120
- } ;
121
-
122
- du = async ( ) => {
123
- return await sudo ( {
124
- command : "btrfs" ,
125
- args : [ "filesystem" , "du" , "-s" , this . path ] ,
126
- } ) ;
127
- } ;
128
-
129
- usage = async ( ) : Promise < {
130
- // used and free in bytes
131
- used : number ;
132
- free : number ;
133
- size : number ;
134
- } > => {
135
- const { stdout } = await sudo ( {
136
- command : "btrfs" ,
137
- args : [ "filesystem" , "usage" , "-b" , this . path ] ,
138
- } ) ;
139
- let used : number = - 1 ;
140
- let free : number = - 1 ;
141
- let size : number = - 1 ;
142
- for ( const x of stdout . split ( "\n" ) ) {
143
- if ( used == - 1 ) {
144
- const i = x . indexOf ( "Used:" ) ;
145
- if ( i != - 1 ) {
146
- used = parseInt ( x . split ( ":" ) [ 1 ] . trim ( ) ) ;
147
- continue ;
148
- }
149
- }
150
- if ( free == - 1 ) {
151
- const i = x . indexOf ( "Free (statfs, df):" ) ;
152
- if ( i != - 1 ) {
153
- free = parseInt ( x . split ( ":" ) [ 1 ] . trim ( ) ) ;
154
- continue ;
155
- }
156
- }
157
- if ( size == - 1 ) {
158
- const i = x . indexOf ( "Device size:" ) ;
159
- if ( i != - 1 ) {
160
- size = parseInt ( x . split ( ":" ) [ 1 ] . trim ( ) ) ;
161
- continue ;
162
- }
163
- }
164
- }
165
- return { used, free, size } ;
166
- } ;
167
-
168
- /////////////
169
- // BTRFS send/recv
170
- // Not used. Instead we will rely on bup (and snapshots of the underlying disk) for backups, since:
171
- // - much easier to check they are valid
172
- // - decoupled from any btrfs issues
173
- // - not tied to any specific filesystem at all
174
- // - easier to offsite via incremntal rsync
175
- // - much more space efficient with *global* dedup and compression
176
- // - bup is really just git, which is very proven
177
- // The drawback is speed.
178
- /////////////
179
-
180
- // this was just a quick proof of concept -- I don't like it. Should switch to using
181
- // timestamps and a lock.
182
- // To recover these, doing recv for each in order does work. Then you have to
183
- // snapshot all of the results to move them. It's awkward, but efficient
184
- // and works fine.
185
- send = async ( ) => {
186
- await mkdirp ( [ join ( this . filesystem . streams , this . name ) ] ) ;
187
- const streams = new Set (
188
- await listdir ( join ( this . filesystem . streams , this . name ) ) ,
189
- ) ;
190
- const allSnapshots = ( await this . snapshots . ls ( ) ) . map ( ( x ) => x . name ) ;
191
- const snapshots = allSnapshots . filter (
192
- ( x ) => x . startsWith ( SEND_SNAPSHOT_PREFIX ) && streams . has ( x ) ,
193
- ) ;
194
- const nums = snapshots . map ( ( x ) =>
195
- parseInt ( x . slice ( SEND_SNAPSHOT_PREFIX . length ) ) ,
196
- ) ;
197
- nums . sort ( ) ;
198
- const last = nums . slice ( - 1 ) [ 0 ] ;
199
- let seq , parent ;
200
- if ( last ) {
201
- seq = `${ last + 1 } ` . padStart ( PAD , "0" ) ;
202
- const l = `${ last } ` . padStart ( PAD , "0" ) ;
203
- parent = `${ SEND_SNAPSHOT_PREFIX } ${ l } ` ;
204
- } else {
205
- seq = "1" . padStart ( PAD , "0" ) ;
206
- parent = "" ;
207
- }
208
- const send = `${ SEND_SNAPSHOT_PREFIX } ${ seq } ` ;
209
- if ( allSnapshots . includes ( send ) ) {
210
- await this . snapshots . delete ( send ) ;
211
- }
212
- await this . snapshots . create ( send ) ;
213
- await sudo ( {
214
- command : "btrfs" ,
215
- args : [
216
- "send" ,
217
- "--compressed-data" ,
218
- join ( this . snapshots . path ( ) , send ) ,
219
- ...( last ? [ "-p" , this . snapshots . path ( parent ) ] : [ ] ) ,
220
- "-f" ,
221
- join ( this . filesystem . streams , this . name , send ) ,
222
- ] ,
223
- } ) ;
224
- if ( parent ) {
225
- await this . snapshots . delete ( parent ) ;
226
- }
227
- } ;
228
-
229
- // recv = async (target: string) => {
230
- // const streamsDir = join(this.filesystem.streams, this.name);
231
- // const streams = await listdir(streamsDir);
232
- // streams.sort();
233
- // for (const stream of streams) {
234
- // await sudo({
235
- // command: "btrfs",
236
- // args: ["recv", "-f", join(streamsDir, stream)],
237
- // });
238
- // }
239
- // };
240
83
}
241
84
242
85
const cache = refCache < Options & { noCache ?: boolean } , Subvolume > ( {
0 commit comments