summaryrefslogtreecommitdiff
path: root/frontend/lib/drive_server.ts
blob: 2a94fe96d71c767c98b44f1229cea01e832ff9c9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
import { spawnSync } from 'child_process'
import { DriveLsEntry, DriveTreeNode, DriveTreeResponse, DriveLogEntry } from './drive_types'
import { Drive_split_path, Drive_basename } from './drive_shared'

/**
 * Server-only drive functions that use Node.js APIs
 */

/// lists the given path on the drive
export async function Drive_ls(path: string, recursive: boolean): Promise<DriveLsEntry[]> {
  const args = ['ls']
  if (recursive) {
    args.push('-r')
  }
  if (path) {
    args.push(path)
  }

  const result = spawnSync('fctdrive', args, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 })
  if (result.error) {
    throw new Error(`Failed to execute fctdrive: ${result.error.message}`)
  }
  if (result.status !== 0) {
    throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`)
  }
  const stdout = result.stdout
  const entries = []
  for (const line of stdout.split('\n')) {
    if (line.trim() == "")
      continue;

    const parts = line.split('\t');
    const path = parts[0];
    const type = parts[1];
    const lastmod = parseInt(parts[2]);
    const blobStr = parts[3];
    const sizeStr = parts[4];
    const author = parts[5];

    let blob = null;
    if (blobStr != "-")
      blob = blobStr;

    let size = null;
    if (sizeStr != "-")
      size = parseFloat(sizeStr);

    entries.push({
      path, type, lastmod, blob, size, author
    } as DriveLsEntry);
  }
  return entries;
}

/// import the file at local_path by moving it to drive_path
export async function Drive_import(local_path: string, drive_path: string, email: string) {
  const result = spawnSync('fctdrive', ['import', local_path, "--mode", "move", "--destination", drive_path, "--email", email], { encoding: 'utf-8' });
  if (result.error) {
    throw new Error(`Failed to execute fctdrive: ${result.error.message}`);
  }
  if (result.status !== 0) {
    throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`);
  }
}

/// returns the full filesystem path of the blob given its id
export async function Drive_blob_path(blob: string): Promise<string> {
  const result = spawnSync('fctdrive', ['blob', blob], { encoding: 'utf-8' })
  if (result.error) {
    throw new Error(`Failed to execute fctdrive: ${result.error.message}`)
  }
  if (result.status !== 0) {
    throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`)
  }
  return result.stdout.trim();
}

/// removes the file or directory at the given path
export async function Drive_remove(path: string, email: string) {
  const result = spawnSync('fctdrive', ['remove', '--email', email, path], { encoding: 'utf-8' });
  if (result.error) {
    throw new Error(`Failed to execute fctdrive: ${result.error.message}`);
  }
  if (result.status !== 0) {
    throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`);
  }
}

/// creates a directory at the given path
export async function Drive_mkdir(path: string, email: string) {
  const result = spawnSync('fctdrive', ['mkdir', '--email', email, '--path', path], { encoding: 'utf-8' });
  if (result.error) {
    throw new Error(`Failed to execute fctdrive: ${result.error.message}`);
  }
  if (result.status !== 0) {
    throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`);
  }
}

/// renames a file or directory from old path to new path
export async function Drive_rename(oldPath: string, newPath: string, email: string) {
  const result = spawnSync('fctdrive', ['rename', '--email', email, '--old', oldPath, '--new', newPath], { encoding: 'utf-8' });
  if (result.error) {
    throw new Error(`Failed to execute fctdrive: ${result.error.message}`);
  }
  if (result.status !== 0) {
    throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`);
  }
}

/// builds a filesystem tree from Drive_ls entries
export async function Drive_tree(): Promise<DriveTreeResponse> {
  const entries = await Drive_ls('/', true);
  
  const nodesMap = new Map<string, DriveTreeNode>();
  const rootNodes: DriveTreeNode[] = [];

  // First pass: create all nodes
  entries.forEach(entry => {
    const node: DriveTreeNode = {
      path: entry.path,
      name: Drive_basename(entry.path),
      type: entry.type,
      lastmod: entry.lastmod,
      blob: entry.blob,
      size: entry.size,
      author: entry.author,
      children: entry.type === "dir" ? [] : undefined
    };
    nodesMap.set(entry.path, node);
  });

  // Second pass: build hierarchy
  entries.forEach(entry => {
    const pathParts = Drive_split_path(entry.path);
    const node = nodesMap.get(entry.path)!;
    
    if (pathParts.length === 1) {
      // Root level item
      rootNodes.push(node);
    } else {
      // Find parent path by reconstructing it
      const parentParts = pathParts.slice(0, -1);
      const parentPath = '/' + parentParts.join('/');
      const parent = nodesMap.get(parentPath);
      
      if (parent && parent.children) {
        parent.children.push(node);
      } else {
        // If parent not found, add to root
        rootNodes.push(node);
      }
    }
  });

  // Third pass: calculate directory sizes and sort all levels
  const calculateSizesAndSort = (nodes: DriveTreeNode[]): DriveTreeNode[] => {
    // First, recursively process children and calculate their sizes
    nodes.forEach(node => {
      if (node.children) {
        node.children = calculateSizesAndSort(node.children);
        
        // If this is a directory, calculate its size as sum of all children
        if (node.type === "dir") {
          node.size = node.children.reduce((total, child) => {
            return total + (child.size || 0);
          }, 0);
        }
      }
    });

    // Then sort: directories first, then files, both alphabetically
    const sorted = nodes.sort((a, b) => {
      // Directories first, then files
      if (a.type === "dir" && b.type === "file") return -1;
      if (a.type === "file" && b.type === "dir") return 1;
      
      // Both same type, sort alphabetically by name (case-insensitive)
      return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
    });

    return sorted;
  };

  return { root: calculateSizesAndSort(rootNodes) };
}

/// lists only directories (recursively) from the given path
export async function Drive_ls_directories(path: string = '/'): Promise<DriveLsEntry[]> {
  // Get all entries recursively and filter for directories
  const allEntries = await Drive_ls(path, true)
  return allEntries.filter(entry => entry.type === 'dir')
}

/// returns the log entries from the drive
export async function Drive_log(): Promise<DriveLogEntry[]> {
  const result = spawnSync('fctdrive', ['log'], { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 })
  if (result.error) {
    throw new Error(`Failed to execute fctdrive: ${result.error.message}`)
  }
  if (result.status !== 0) {
    throw new Error(`fctdrive exited with code ${result.status}: ${result.stderr}`)
  }
  
  const stdout = result.stdout
  const entries = []
  for (const line of stdout.split('\n')) {
    if (line.trim() === "")
      continue;

    const parts = line.split('\t');
    const timestamp = parseInt(parts[0]);
    const log_id = parseInt(parts[1]);
    const email = parts[2];
    const action = parts[3];
    const path = parts[4];
    const blob_id = parts[5];
    const size = parseInt(parts[6]);

    entries.push({
      timestamp, log_id, email, action, path, blob_id, size
    } as DriveLogEntry);
  }
  return entries;
}