Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/util/db-schema/projects.ts
5716 views
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { State } from "@cocalc/util/compute-states";
7
import { PurchaseInfo } from "@cocalc/util/licenses/purchase/types";
8
import { deep_copy } from "@cocalc/util/misc";
9
import {
10
ExecuteCodeOptions,
11
ExecuteCodeOptionsAsyncGet,
12
ExecuteCodeOutput,
13
} from "@cocalc/util/types/execute-code";
14
import { type RegistrationTokenCustomize } from "@cocalc/util/types/registration-token";
15
import { DEFAULT_QUOTAS } from "@cocalc/util/upgrade-spec";
16
import { isUserGroup } from "@cocalc/util/project-ownership";
17
18
import { NOTES } from "./crm";
19
import { FALLBACK_COMPUTE_IMAGE } from "./defaults";
20
import { SCHEMA as schema } from "./index";
21
import { callback2 } from "@cocalc/util/async-utils";
22
import { Table } from "./types";
23
24
export const MAX_FILENAME_SEARCH_RESULTS = 100;
25
26
const PROJECTS_LIMIT = 300;
27
const PROJECTS_CUTOFF = "6 weeks";
28
const THROTTLE_CHANGES = 1000;
29
30
Table({
31
name: "projects",
32
rules: {
33
primary_key: "project_id",
34
//# A lot depends on this being right at all times, e.g., restart state,
35
//# so do not use db_standby yet.
36
//# It is simply not robust enough.
37
//# db_standby : 'safer'
38
39
pg_indexes: [
40
"last_edited",
41
"created", // TODO: this could have a fillfactor of 100
42
"USING GIN (users)", // so get_collaborator_ids is fast
43
"lti_id",
44
"USING GIN (state)", // so getting all running projects is fast (e.g. for site_license_usage_log... but also manage-state)
45
"((state #>> '{state}'))", // projecting the "state" (running, etc.) for its own index – the GIN index above still causes a scan, which we want to avoid.
46
"((state ->> 'state'))", // same reason as above. both syntaxes appear and we have to index both.
47
"((state IS NULL))", // not covered by the above
48
"((settings ->> 'always_running'))", // to quickly know which projects have this setting
49
"((run_quota ->> 'always_running'))", // same reason as above
50
"deleted", // in various queries we quickly fiter deleted projects
51
"site_license", // for queries across projects related to site_license#>>{license_id}
52
],
53
54
crm_indexes: ["last_edited"],
55
56
user_query: {
57
get: {
58
pg_where: [
59
`last_edited >= NOW() - interval '${PROJECTS_CUTOFF}'`,
60
"projects",
61
],
62
pg_where_load: ["last_edited >= NOW() - interval '7 days'", "projects"],
63
options: [{ limit: PROJECTS_LIMIT, order_by: "-last_edited" }],
64
options_load: [{ limit: 50, order_by: "-last_edited" }],
65
pg_changefeed: "projects",
66
throttle_changes: THROTTLE_CHANGES,
67
fields: {
68
project_id: null,
69
name: null,
70
title: "",
71
description: "",
72
users: {},
73
invite: null, // who has been invited to this project via email
74
invite_requests: null, // who has requested to be invited
75
deleted: null,
76
host: null,
77
settings: DEFAULT_QUOTAS,
78
run_quota: null,
79
site_license: null,
80
status: null,
81
manage_users_owner_only: null,
82
// security model is anybody with access to the project should be allowed to know this token.
83
secret_token: null,
84
state: null,
85
last_edited: null,
86
last_active: null,
87
action_request: null, // last requested action -- {action:?, time:?, started:?, finished:?, err:?}
88
course: null,
89
// if the value is not set, we have to use the old default prior to summer 2020 (Ubuntu 18.04, not 20.04!)
90
compute_image: FALLBACK_COMPUTE_IMAGE,
91
created: null,
92
ephemeral: null,
93
env: null,
94
sandbox: null,
95
avatar_image_tiny: null,
96
// do NOT add avatar_image_full here or it will get included in changefeeds, which we don't want.
97
// instead it gets its own virtual table.
98
color: null,
99
pay_as_you_go_quotas: null,
100
},
101
},
102
set: {
103
// NOTE: for security reasons users CANNOT set the course field via a user query;
104
// instead use the api/v2/projects/course/set-course-field api endpoint.
105
fields: {
106
project_id: "project_write",
107
title: true,
108
name: true,
109
description: true,
110
deleted: true,
111
invite_requests: true, // project collabs can modify this (e.g., to remove from it once user added or rejected)
112
users(obj, db, account_id) {
113
return db._user_set_query_project_users(obj, account_id);
114
},
115
manage_users_owner_only(obj, db, account_id) {
116
return db._user_set_query_project_manage_users_owner_only(
117
obj,
118
account_id,
119
);
120
},
121
action_request: true, // used to request that an action be performed, e.g., "save"; handled by before_change
122
compute_image: true,
123
site_license: true,
124
env: true,
125
sandbox: true,
126
avatar_image_tiny: true,
127
avatar_image_full: true,
128
color: true,
129
},
130
required_fields: {
131
project_id: true,
132
},
133
async check_hook(db, obj, account_id, _project_id, cb) {
134
// Validate manage_users_owner_only permission if it's being changed
135
if (obj.manage_users_owner_only !== undefined) {
136
try {
137
// Require actor identity before hitting the database
138
if (!account_id) {
139
throw Error(
140
"account_id is required to change manage_users_owner_only",
141
);
142
}
143
144
const siteSettings =
145
(await callback2(db.get_server_settings_cached, {})) ?? {};
146
const siteEnforced = !!siteSettings.strict_collaborator_management;
147
if (siteEnforced && obj.manage_users_owner_only !== true) {
148
throw Error(
149
"Collaborator management is enforced by the site administrator and cannot be disabled.",
150
);
151
}
152
153
const { rows } = await db.async_query({
154
query: "SELECT users FROM projects WHERE project_id = $1",
155
params: [obj.project_id],
156
});
157
const users = rows?.[0]?.users ?? {};
158
159
// Check that the user making the change is an owner
160
const group = users?.[account_id]?.group;
161
if (!isUserGroup(group) || group !== "owner") {
162
throw Error(
163
"Only project owners can change collaborator management settings",
164
);
165
}
166
} catch (err) {
167
cb(err.toString());
168
return;
169
}
170
}
171
cb();
172
},
173
before_change(database, old_val, new_val, account_id, cb) {
174
database._user_set_query_project_change_before(
175
old_val,
176
new_val,
177
account_id,
178
cb,
179
);
180
},
181
182
on_change(database, old_val, new_val, account_id, cb) {
183
database._user_set_query_project_change_after(
184
old_val,
185
new_val,
186
account_id,
187
cb,
188
);
189
},
190
},
191
},
192
193
project_query: {
194
get: {
195
pg_where: [{ "project_id = $::UUID": "project_id" }],
196
fields: {
197
project_id: null,
198
title: null,
199
description: null,
200
status: null,
201
},
202
},
203
set: {
204
fields: {
205
project_id: "project_id",
206
title: true,
207
description: true,
208
status: true,
209
},
210
},
211
},
212
},
213
fields: {
214
project_id: {
215
type: "uuid",
216
desc: "The project id, which is the primary key that determines the project.",
217
},
218
name: {
219
type: "string",
220
pg_type: "VARCHAR(100)",
221
desc: "The optional name of this project. Must be globally unique (up to case) across all projects with a given *owner*. It can be between 1 and 100 characters from a-z A-Z 0-9 period and dash.",
222
render: { type: "text", maxLen: 100, editable: true },
223
},
224
title: {
225
type: "string",
226
desc: "The short title of the project. Should use no special formatting, except hashtags.",
227
render: { type: "project_link", project_id: "project_id" },
228
},
229
description: {
230
type: "string",
231
desc: "A longer textual description of the project. This can include hashtags and should be formatted using markdown.",
232
render: {
233
type: "markdown",
234
maxLen: 1024,
235
editable: true,
236
},
237
}, // markdown rendering possibly not implemented
238
users: {
239
title: "Collaborators",
240
type: "map",
241
desc: "This is a map from account_id's to {hide:bool, group:'owner'|'collaborator', upgrades:{memory:1000, ...}, ssh:{...}}.",
242
render: { type: "usersmap", editable: true },
243
},
244
manage_users_owner_only: {
245
type: "boolean",
246
desc: "If true, only project owners can add or remove collaborators. Collaborators can still remove themselves. Disabled by default (undefined or false means current behavior where collaborators can manage other collaborators).",
247
render: { type: "boolean", editable: true },
248
},
249
invite: {
250
type: "map",
251
desc: "Map from email addresses to {time:when invite sent, error:error message if there was one}",
252
date: ["time"],
253
},
254
invite_requests: {
255
type: "map",
256
desc: "This is a map from account_id's to {timestamp:?, message:'i want to join because...'}.",
257
date: ["timestamp"],
258
},
259
deleted: {
260
type: "boolean",
261
desc: "Whether or not this project is deleted.",
262
render: { type: "boolean", editable: true },
263
},
264
host: {
265
type: "map",
266
desc: "This is a map {host:'hostname_of_server', assigned:timestamp of when assigned to that server}.",
267
date: ["assigned"],
268
},
269
settings: {
270
type: "map",
271
desc: 'This is a map that defines the free base quotas that a project has. It is of the form {cores: 1.5, cpu_shares: 768, disk_quota: 1000, memory: 2000, mintime: 36000000, network: 0, ephemeral_state:0, ephemeral_disk:0, always_running:0}. WARNING: some of the values are strings not numbers in the database right now, e.g., disk_quota:"1000".',
272
},
273
site_license: {
274
type: "map",
275
desc: "This is a map that defines upgrades (just when running the project) that come from a site license, and also the licenses that are applied to this project. The format is {license_id:{memory:?, mintime:?, ...}} where the target of the license_id is the same as for the settings field. The license_id is the uuid of the license that contributed these upgrades. To tell cocalc to use a license for a project, a user sets site_license to {license_id:{}}, and when it is requested to start the project, the backend decides what allocation license_id provides and changes the field accordingly, i.e., changes {license_id:{},...} to {license_id:{memory:?,...},...}",
276
},
277
status: {
278
type: "map",
279
desc: "This is a map computed by the status command run inside a project, and slightly enhanced by the compute server, which gives extensive status information about a project. See the exported ProjectStatus interface defined in the code here.",
280
},
281
state: {
282
type: "map",
283
desc: 'Info about the state of this project of the form {error: "", state: "running" (etc), time: timestamp, ip?:"ip address where project is"}, where time is when the state was last computed. See COMPUTE_STATES in the compute-states file for state.state and the ProjectState interface defined below in code.',
284
date: ["time"],
285
},
286
last_edited: {
287
type: "timestamp",
288
desc: "The last time some file was edited in this project. This is the last time that the file_use table was updated for this project.",
289
},
290
last_started: {
291
type: "timestamp",
292
desc: "The last time the project started running.",
293
},
294
last_active: {
295
type: "map",
296
desc: "Map from account_id's to the timestamp of when the user with that account_id touched this project.",
297
date: "all",
298
},
299
created: {
300
type: "timestamp",
301
desc: "When the project was created.",
302
},
303
ephemeral: {
304
type: "number",
305
desc: "If set, number of milliseconds this project may exist after creation.",
306
},
307
action_request: {
308
type: "map",
309
desc: "Request state change action for project: {action:['start', 'stop'], started:timestamp, err:?, finished:timestamp}",
310
date: ["started", "finished"],
311
},
312
storage: {
313
type: "map",
314
desc: "(DEPRECATED) This is a map {host:'hostname_of_server', assigned:when first saved here, saved:when last saved here}.",
315
date: ["assigned", "saved"],
316
},
317
last_backup: {
318
type: "timestamp",
319
desc: "(DEPRECATED) Timestamp of last off-disk successful backup using bup to Google cloud storage",
320
},
321
storage_request: {
322
type: "map",
323
desc: "(DEPRECATED) {action:['save', 'close', 'move', 'open'], requested:timestap, pid:?, target:?, started:timestamp, finished:timestamp, err:?}",
324
date: ["started", "finished", "requested"],
325
},
326
course: {
327
type: "map",
328
desc: "{project_id:[id of project that contains .course file], path:[path to .course file], pay:?, payInfo:?, email_address:[optional email address of student -- used if account_id not known], account_id:[account id of student]}, where pay is either not set (or equals falseish) or is a timestamp by which the students must pay. If payInfo is set, it specifies the parameters of the license the students should purchase.",
329
date: ["pay"],
330
},
331
storage_server: {
332
type: "integer",
333
desc: "(DEPRECATED) Number of the Kubernetes storage server with the data for this project: one of 0, 1, 2, ...",
334
},
335
storage_ready: {
336
type: "boolean",
337
desc: "(DEPRECATED) Whether storage is ready to be used on the storage server. Do NOT try to start project until true; this gets set by storage daemon when it notices that run is true.",
338
},
339
disk_size: {
340
type: "integer",
341
desc: "Size in megabytes of the project disk.",
342
},
343
resources: {
344
type: "map",
345
desc: 'Object of the form {requests:{memory:"30Mi",cpu:"5m"}, limits:{memory:"100Mi",cpu:"300m"}} which is passed to the k8s resources section for this pod.',
346
},
347
preemptible: {
348
type: "boolean",
349
desc: "If true, allow to run on preemptible nodes.",
350
},
351
idle_timeout: {
352
type: "integer",
353
desc: "If given and nonzero, project will be killed if it is idle for this many **minutes**, where idle *means* that last_edited has not been updated.",
354
},
355
run_quota: {
356
type: "map",
357
desc: "If project is running, this is the quota that it is running with.",
358
},
359
compute_image: {
360
type: "string",
361
desc: "Specify the name of the underlying (kucalc) compute image.",
362
},
363
addons: {
364
type: "map",
365
desc: "Configure (kucalc specific) addons for projects. (e.g. academic software, license keys, ...)",
366
},
367
lti_id: {
368
type: "array",
369
pg_type: "TEXT[]",
370
desc: "This is a specific ID derived from an LTI context",
371
},
372
lti_data: {
373
type: "map",
374
desc: "extra information related to LTI",
375
},
376
env: {
377
type: "map",
378
desc: "Additional environment variables (TS: {[key:string]:string})",
379
render: { type: "json", editable: true },
380
},
381
sandbox: {
382
type: "boolean",
383
desc: "If set to true, then any user who attempts to access this project is automatically added as a collaborator to it. Only the project owner can change this setting.",
384
render: { type: "boolean", editable: true },
385
},
386
avatar_image_tiny: {
387
title: "Image",
388
type: "string",
389
desc: "tiny (32x32) visual image associated with the project. Suitable to include as part of changefeed, since about 3kb.",
390
render: { type: "image" },
391
},
392
avatar_image_full: {
393
title: "Image",
394
type: "string",
395
desc: "A visual image associated with the project. Could be 150kb. NOT include as part of changefeed of projects, since potentially big (e.g., 200kb x 1000 projects = 200MB!).",
396
render: { type: "image" },
397
},
398
color: {
399
title: "Color",
400
type: "string",
401
desc: "Optional color associated with the project, used for visual identification (e.g., border color in project list).",
402
render: { type: "text" },
403
},
404
pay_as_you_go_quotas: {
405
type: "map",
406
desc: "Pay as you go quotas that users set so that when they run this project, it gets upgraded to at least what is specified here, and user gets billed later for what is used. Any changes to this table could result in money being spent, so should only be done via the api. This is a map from the account_id of the user that set the quota to the value of the quota spec (which is purchase-quotas.ProjectQuota).",
407
render: { type: "json", editable: false },
408
},
409
notes: NOTES,
410
secret_token: {
411
type: "string",
412
pg_type: "VARCHAR(256)",
413
desc: "Random ephemeral secret token used temporarily by project to authenticate with hub.",
414
},
415
},
416
});
417
418
export interface ApiKeyInfo {
419
name: string;
420
trunc: string;
421
hash?: string;
422
used?: number;
423
}
424
425
// Same query above, but without the last_edited time constraint.
426
schema.projects_all = deep_copy(schema.projects);
427
if (
428
schema.projects_all.user_query?.get == null ||
429
schema.projects.user_query?.get == null
430
) {
431
throw Error("make typescript happy");
432
}
433
schema.projects_all.user_query.get.options = [];
434
schema.projects_all.user_query.get.options_load = [];
435
schema.projects_all.virtual = "projects";
436
schema.projects_all.user_query.get.pg_where = ["projects"];
437
schema.projects_all.user_query.get.pg_where_load = ["projects"];
438
439
// Table that provides extended read info about a single project
440
// but *ONLY* for admin.
441
Table({
442
name: "projects_admin",
443
fields: schema.projects.fields,
444
rules: {
445
primary_key: schema.projects.primary_key,
446
virtual: "projects",
447
user_query: {
448
get: {
449
admin: true, // only admins can do get queries on this table
450
// (without this, users who have read access could read)
451
pg_where: [{ "project_id = $::UUID": "project_id" }],
452
fields: schema.projects.user_query.get.fields,
453
},
454
},
455
},
456
});
457
458
/*
459
Table that enables set queries to the course field of a project. Only
460
project owners are allowed to use this table. The point is that this makes
461
it possible for the owner of the project to set things, but not for the
462
collaborators to set those things.
463
**wARNING:** right now we're not using this since when multiple people add
464
students to a course and the 'course' field doesn't get properly set,
465
much confusion and misery arises.... and it is very hard to fix.
466
In theory a malicous student could not pay via this. But if they could
467
mess with their client, they could easily not pay anyways.
468
*/
469
Table({
470
name: "projects_owner",
471
rules: {
472
virtual: "projects",
473
user_query: {
474
set: {
475
fields: {
476
project_id: "project_owner",
477
course: true,
478
},
479
},
480
},
481
},
482
fields: {
483
project_id: true,
484
course: true,
485
},
486
});
487
488
/*
489
490
Table that enables any signed-in user to set an invite request.
491
Later: we can make an index so that users can see all outstanding requests they have made easily.
492
How to test this from the browser console:
493
project_id = '4e0f5bfd-3f1b-4d7b-9dff-456dcf8725b8' // id of a project you have
494
invite_requests = {}; invite_requests[smc.client.account_id] = {timestamp:new Date(), message:'please invite me'}
495
smc.client.query({cb:console.log, query:{project_invite_requests:{project_id:project_id, invite_requests:invite_requests}}}) // set it
496
smc.redux.getStore('projects').get_project(project_id).invite_requests // see requests for this project
497
498
CURRENTLY NOT USED, but probably will be...
499
500
database._user_set_query_project_invite_requests(old_val, new_val, account_id, cb)
501
For now don't check anything -- this is how we will make it secure later.
502
This will:
503
- that user setting this is signed in
504
- ensure user only modifies their own entry (for their own id).
505
- enforce some hard limit on number of outstanding invites (say 30).
506
- enforce limit on size of invite message.
507
- sanity check on timestamp
508
- with an index as mentioned above we could limit the number of projects
509
to which a single user has requested to be invited.
510
511
*/
512
Table({
513
name: "project_invite_requests",
514
rules: {
515
virtual: "projects",
516
primary_key: "project_id",
517
user_query: {
518
set: {
519
fields: {
520
project_id: true,
521
invite_requests: true,
522
},
523
before_change(_database, _old_val, _new_val, _account_id, cb) {
524
cb();
525
},
526
},
527
},
528
}, // actual function will be database._user...
529
fields: {
530
project_id: true,
531
invite_requests: true,
532
}, // {account_id:{timestamp:?, message:?}, ...}
533
});
534
535
/*
536
Virtual table to get project avatar_images.
537
We don't put this in the main projects table,
538
since we don't want the avatar_image_full to be
539
the projects queries or changefeeds, since it
540
is big, and by default all get fields appear there.
541
*/
542
543
Table({
544
name: "project_avatar_images",
545
rules: {
546
virtual: "projects",
547
primary_key: "project_id",
548
user_query: {
549
get: {
550
pg_where: ["projects"],
551
fields: {
552
project_id: null,
553
avatar_image_full: null,
554
},
555
},
556
},
557
},
558
fields: {
559
project_id: true,
560
avatar_image_full: true,
561
},
562
});
563
564
/*
565
Table to get/set the datastore config in addons.
566
567
The main idea is to set/update/delete entries in the dict addons.datastore.[key] = {...}
568
*/
569
Table({
570
name: "project_datastore",
571
rules: {
572
virtual: "projects",
573
primary_key: "project_id",
574
user_query: {
575
set: {
576
// this also deals with delete requests
577
fields: {
578
project_id: true,
579
addons: true,
580
},
581
async instead_of_change(
582
db,
583
_old_value,
584
new_val,
585
account_id,
586
cb,
587
): Promise<void> {
588
try {
589
// to delete an entry, pretend to set the datastore = {delete: [name]}
590
if (typeof new_val.addons.datastore.delete === "string") {
591
await db.project_datastore_del(
592
account_id,
593
new_val.project_id,
594
new_val.addons.datastore.delete,
595
);
596
cb(undefined);
597
} else {
598
// query should set addons.datastore.[new key] = config, such that we see here
599
// new_val = {"project_id":"...","addons":{"datastore":{"key3":{"type":"xxx", ...}}}}
600
// which will be merged into the existing addons.datastore dict
601
const res = await db.project_datastore_set(
602
account_id,
603
new_val.project_id,
604
new_val.addons.datastore,
605
);
606
cb(undefined, res);
607
}
608
} catch (err) {
609
cb(`${err}`);
610
}
611
},
612
},
613
get: {
614
fields: {
615
project_id: true,
616
addons: true,
617
},
618
async instead_of_query(db, opts, cb): Promise<void> {
619
if (opts.multi) {
620
throw Error("'multi' is not implemented");
621
}
622
try {
623
// important: the config dicts for each key must not expose secret credentials!
624
// check if opts.query.addons === null ?!
625
const data = await db.project_datastore_get(
626
opts.account_id,
627
opts.query.project_id,
628
);
629
cb(undefined, data);
630
} catch (err) {
631
cb(`${err}`);
632
}
633
},
634
},
635
},
636
},
637
fields: {
638
project_id: true,
639
addons: true,
640
},
641
});
642
643
export interface ProjectStatus {
644
"project.pid"?: number; // pid of project server process
645
"hub-server.port"?: number; // port of tcp server that is listening for conn from hub
646
"browser-server.port"?: number; // port listening for http/websocket conn from browser client
647
"sage_server.port"?: number; // port where sage server is listening.
648
"sage_server.pid"?: number; // pid of sage server process
649
start_ts?: number; // timestamp, when project server started
650
session_id?: string; // unique identifyer
651
version?: number; // version number of project code
652
disk_MB?: number; // MB of used disk
653
installed?: boolean; // whether code is installed
654
memory?: {
655
count?: number;
656
pss?: number;
657
rss?: number;
658
swap?: number;
659
uss?: number;
660
}; // output by smem
661
}
662
663
export interface ProjectState {
664
ip?: string; // where the project is running
665
error?: string;
666
state?: State; // running, stopped, etc.
667
time?: Date;
668
}
669
670
Table({
671
name: "crm_projects",
672
fields: schema.projects.fields,
673
rules: {
674
primary_key: schema.projects.primary_key,
675
virtual: "projects",
676
user_query: {
677
get: {
678
admin: true, // only admins can do get queries on this table
679
// (without this, users who have read access could read)
680
pg_where: [],
681
fields: {
682
...schema.projects.user_query?.get?.fields,
683
notes: null,
684
},
685
},
686
set: {
687
admin: true,
688
fields: {
689
project_id: true,
690
name: true,
691
title: true,
692
description: true,
693
deleted: true,
694
notes: true,
695
},
696
},
697
},
698
},
699
});
700
701
export type Datastore = boolean | string[] | undefined;
702
703
// in the future, we might want to extend this to include custom environmment variables
704
export interface EnvVarsRecord {
705
inherit?: boolean;
706
}
707
export type EnvVars = EnvVarsRecord | undefined;
708
709
export interface StudentProjectFunctionality {
710
disableActions?: boolean;
711
disableJupyterToggleReadonly?: boolean;
712
disableJupyterClassicServer?: boolean;
713
disableJupyterClassicMode?: boolean;
714
disableJupyterLabServer?: boolean;
715
disableRServer?: boolean;
716
disableVSCodeServer?: boolean;
717
disableLibrary?: boolean;
718
disableNetworkWarningBanner?: boolean;
719
disablePlutoServer?: boolean;
720
disableTerminals?: boolean;
721
disableUploads?: boolean;
722
disableNetwork?: boolean;
723
disableSSH?: boolean;
724
disableCollaborators?: boolean;
725
disableChatGPT?: boolean;
726
disableSharing?: boolean;
727
}
728
729
export interface CourseInfo {
730
type: "student" | "shared" | "nbgrader";
731
account_id?: string; // account_id of the student that this project is for.
732
project_id: string; // the course project, i.e., project with the .course file
733
path: string; // path to the .course file in project_id
734
pay?: string; // iso timestamp or ""
735
paid?: string; // iso timestamp with *when* they paid.
736
purchase_id?: number; // id of purchase record in purchases table.
737
payInfo?: PurchaseInfo;
738
email_address?: string;
739
datastore: Datastore;
740
student_project_functionality?: StudentProjectFunctionality;
741
envvars?: EnvVars;
742
}
743
744
type ExecOptsCommon = {
745
project_id: string;
746
cb?: Function; // if given use a callback interface *instead* of async.
747
};
748
749
export type ExecOptsBlocking = ExecOptsCommon & {
750
compute_server_id?: number; // if true, run on the compute server (if available)
751
filesystem?: boolean; // run in fileserver container on compute server; otherwise, runs on main compute container.
752
path?: string;
753
command: string;
754
args?: string[];
755
timeout?: number;
756
max_output?: number;
757
bash?: boolean;
758
aggregate?: string | number | { value: string | number };
759
err_on_exit?: boolean;
760
env?: { [key: string]: string }; // custom environment variables.
761
async_call?: ExecuteCodeOptions["async_call"];
762
};
763
764
export type ExecOptsAsync = ExecOptsCommon & {
765
async_get?: ExecuteCodeOptionsAsyncGet["async_get"];
766
async_stats?: ExecuteCodeOptionsAsyncGet["async_stats"];
767
async_await?: ExecuteCodeOptionsAsyncGet["async_await"];
768
};
769
770
export type ExecOpts = ExecOptsBlocking | ExecOptsAsync;
771
772
export function isExecOptsBlocking(opts: unknown): opts is ExecOptsBlocking {
773
return (
774
typeof opts === "object" &&
775
typeof (opts as any).project_id === "string" &&
776
typeof (opts as any).command === "string"
777
);
778
}
779
780
export type ExecOutput = ExecuteCodeOutput & {
781
time: number; // time in ms, from user point of view.
782
};
783
784
export interface CreateProjectOptions {
785
account_id?: string;
786
title?: string;
787
description?: string;
788
// (optional) image ID
789
image?: string;
790
// (optional) license id (or multiple ids separated by commas) -- if given, project will be created with this license
791
license?: string;
792
public_path_id?: string; // may imply use of a license
793
// noPool = do not allow using the pool (e.g., need this when creating projects to put in the pool);
794
// not a real issue since when creating for pool account_id is null, and then we wouldn't use the pool...
795
noPool?: boolean;
796
// start running the moment the project is created -- uses more resources, but possibly better user experience
797
start?: boolean;
798
799
// admins can specify the project_id - nobody else can -- useful for debugging.
800
project_id?: string;
801
// if set, project should be treated as expiring after this many milliseconds since creation
802
ephemeral?: number;
803
// account customization settings to apply to project (e.g., disableInternet)
804
customize?: RegistrationTokenCustomize;
805
}
806
807
interface BaseCopyOptions {
808
target_project_id?: string;
809
target_path?: string; // path into project; if not given, defaults to source path above.
810
overwrite_newer?: boolean; // if true, newer files in target are copied over (otherwise, uses rsync's --update)
811
delete_missing?: boolean; // if true, delete files in dest path not in source, **including** newer files
812
backup?: boolean; // make backup files
813
timeout?: number; // in **seconds**, not milliseconds
814
bwlimit?: number;
815
wait_until_done?: boolean; // by default, wait until done. false only gives the ID to query the status later
816
scheduled?: string | Date; // kucalc only: string (parseable by new Date()), or a Date
817
public?: boolean; // kucalc only: if true, may use the share server files rather than start the source project running
818
exclude?: string[]; // options passed to rsync via --exclude
819
}
820
export interface UserCopyOptions extends BaseCopyOptions {
821
account_id?: string;
822
src_project_id: string;
823
src_path: string;
824
// simulate copy taking at least this long -- useful for dev/debugging.
825
debug_delay_ms?: number;
826
}
827
828
// for copying files within and between projects
829
export interface CopyOptions extends BaseCopyOptions {
830
path: string;
831
}
832
833