@trap_stevo/filetide 0.0.32 → 0.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,35 @@
1
+ # License Agreement for @trap_stevo/filetide
2
+
3
+ **Effective Date**: September 10, 2024
4
+
5
+ This License Agreement ("Agreement") is entered into by and between **Steven Compton**, the owner of the package ("Licensor"), and any individual or entity attempting to use the package ("Licensee"). By attempting to install, use, or modify the package, Licensee agrees to the following terms and conditions:
6
+
7
+ ## 1. **Ownership**
8
+ **Steven Compton** is the sole owner of all rights, title, and interest in and to the @trap_stevo/filetide, including all related intellectual property rights.
9
+
10
+ ## 2. **Grant of License**
11
+ Licensee is **NOT** granted any rights to use, modify, distribute, or create derivative works of @trap_stevo/filetide, either in part or in whole, without prior express written consent from the Licensor. Any use of the package without permission constitutes a violation of this Agreement and applicable intellectual property laws.
12
+
13
+ ## 3. **Prohibited Uses**
14
+ Without the express written permission of the Licensor, Licensee shall not:
15
+ - Use the package for any purpose, including but not limited to personal, commercial, or educational purposes.
16
+ - Copy, modify, distribute, or sublicense the package.
17
+ - Reverse-engineer, decompile, or disassemble the package.
18
+
19
+ ## 4. **Request for Permission**
20
+ Licensee may request permission to use the package by contacting the Licensor at **h.steven.compton13@gmail.com**. The Licensor reserves the right to approve or deny any such request at its sole discretion.
21
+
22
+ ## 5. **Violation of Terms**
23
+ Any unauthorized use of @trap_stevo/filetide will result in immediate termination of any rights granted and may lead to legal action. Licensor reserves the right to seek damages and remedies to the fullest extent permitted by law.
24
+
25
+ ## 6. **No Warranty**
26
+ @trap_stevo/filetide is provided "as is," without any warranty of any kind, express or implied, including but not limited to warranties of merchantability, fitness for a particular purpose, or non-infringement. Licensee assumes all risks associated with the use of the package, including but not limited to potential damage to systems or data.
27
+
28
+ ## 7. **Indemnification**
29
+ Licensee agrees to indemnify and hold harmless the Licensor from any and all claims, damages, liabilities, and expenses arising out of Licensee's use of the package in violation of this Agreement.
30
+
31
+ ## 8. **Governing Law**
32
+ This Agreement shall be governed by and construed in accordance with the laws of **Georgia**, without regard to its conflict of law principles.
33
+
34
+ ## 9. **Amendments**
35
+ Licensor reserves the right to modify or update this Agreement at any time. Any changes to the Agreement will be effective immediately upon being posted to **[Location of Agreement, e.g., repository or website]**.
@@ -10,6 +10,7 @@ const {
10
10
  const {
11
11
  FileTransferManager
12
12
  } = require("./HUDManagers/FileTransferManager");
13
+ const HUDTargetQueue = require("./HUDManagers/HUDTargetQueueManager");
13
14
  class FileMessager {
14
15
  constructor(options = {
15
16
  pingTimeout: 3600000,
@@ -20,6 +21,9 @@ class FileMessager {
20
21
  }) {
21
22
  const serverOptions = FileMessagerConfigManager.getServerOptions(options);
22
23
  this.transporter = new FileTransferManager(transportOptions);
24
+ this.transferQueue = new HUDTargetQueue();
25
+ this.currentTransferBarrierTransfers = new Map();
26
+ this.clientTransferBarriers = new Map();
23
27
  this.fileNet = new IoTide(serverOptions.port, serverOptions, true, this.onConnect.bind(this), this.onDisconnect.bind(this));
24
28
  this.onlineClients = new Map();
25
29
  return;
@@ -35,6 +39,10 @@ class FileMessager {
35
39
  return;
36
40
  }
37
41
  start() {
42
+ this.fileNet.on("get-client-transfer-barrier-response", this.handleClientTransferBarrierResponse.bind(this));
43
+ this.fileNet.on("notify-transfer-barrier", this.handleNotifyTransferBarrier.bind(this));
44
+ this.fileNet.on("send-file-list-to-client", this.handleSendFileListToClient.bind(this));
45
+ this.fileNet.on("list-client-files", this.handleListClientFiles.bind(this));
38
46
  this.fileNet.on("transfer-start", this.handleFileTransferStart.bind(this));
39
47
  this.fileNet.on("transfer-progress", this.handleFileTransferProgress.bind(this));
40
48
  this.fileNet.on("transfer-complete", this.handleFileTransferComplete.bind(this));
@@ -63,7 +71,8 @@ class FileMessager {
63
71
  senderID,
64
72
  requesterID,
65
73
  path: filePath,
66
- destinationPath
74
+ destinationPath,
75
+ type: "requestedTransfer"
67
76
  });
68
77
  }
69
78
 
@@ -118,6 +127,41 @@ class FileMessager {
118
127
  } = transferData;
119
128
  this.requestFromClient(requesterID, senderID, filePath, destinationPath);
120
129
  }
130
+ handleSendFileListToClient(listData) {
131
+ const {
132
+ listedFiles,
133
+ recipientID,
134
+ senderID
135
+ } = listData;
136
+ const recipientConnectionID = FileNetClientManager.getOnlineClient(recipientID).id;
137
+ const senderConnectionID = FileNetClientManager.getOnlineClient(senderID).id;
138
+ const senderClient = this.getClientFromID(senderConnectionID);
139
+ const client = this.getClientFromID(recipientConnectionID);
140
+ if (!client || !senderClient) {
141
+ return;
142
+ }
143
+ this.fileNet.emitToTide(client.tideID, "client-file-list", {
144
+ senderID,
145
+ listedFiles
146
+ });
147
+ }
148
+ handleListClientFiles(clientData) {
149
+ const {
150
+ requesterID,
151
+ recipientID
152
+ } = clientData;
153
+ const recipientConnectionID = FileNetClientManager.getOnlineClient(recipientID).id;
154
+ const senderConnectionID = FileNetClientManager.getOnlineClient(requesterID).id;
155
+ const senderClient = this.getClientFromID(senderConnectionID);
156
+ const client = this.getClientFromID(recipientConnectionID);
157
+ if (!client || !senderClient) {
158
+ return;
159
+ }
160
+ this.fileNet.emitToTide(client.tideID, "list-files", {
161
+ senderID: requesterID,
162
+ recipientID
163
+ });
164
+ }
121
165
  handleClientToClientTransfer(transferData) {
122
166
  const {
123
167
  senderID,
@@ -147,9 +191,48 @@ class FileMessager {
147
191
  console.log(`[FileTide ~ File Messager] ~ File transfer completed: ${fileName} from ${senderID} to ${recipientId}`);
148
192
  }
149
193
  }
194
+ handleClientTransferBarrierResponse(data) {
195
+ console.log(`Checking ${data.recipientID}'s transfer barrier for transfer ~ ${data.transferID}...`);
196
+ const recipientConnectionID = FileNetClientManager.getOnlineClient(data.recipientID).id;
197
+ const senderConnectionID = FileNetClientManager.getOnlineClient(data.clientID).id;
198
+ const senderClient = this.getClientFromID(senderConnectionID);
199
+ const client = this.getClientFromID(recipientConnectionID);
200
+ if (!client || !senderClient) {
201
+ return;
202
+ }
203
+ this.fileNet.emitToTide(client.tideID, "queue-incoming-transfer", {
204
+ ...data,
205
+ transferType: "send"
206
+ });
207
+ this.transferQueue.waitForData(data.transferID, () => this.currentTransferBarrierTransfers.get(data.transferID), [true, false], 100, 50000).then(accepted => {
208
+ console.log(`Transfer ${accepted ? "accepted!" : "denied."}`);
209
+ this.fileNet.emitToTide(senderClient.tideID, "transfer-barrier-response", accepted);
210
+ this.currentTransferBarrierTransfers.delete(data.transferID);
211
+ if (accepted) {
212
+ this.fileNet.emitToTide(client.tideID, "accepted-transfer", {
213
+ accepted,
214
+ ...data
215
+ });
216
+ return;
217
+ }
218
+ this.fileNet.emitToTide(client.tideID, "denied-transfer", {
219
+ accepted,
220
+ ...data
221
+ });
222
+ }).catch(error => {
223
+ console.error(error.message);
224
+ });
225
+ return;
226
+ }
227
+ handleNotifyTransferBarrier(data, emitToChannel) {
228
+ const accepted = data.accepted;
229
+ const transferID = data.data.transferID;
230
+ this.currentTransferBarrierTransfers.set(transferID, accepted);
231
+ }
150
232
  handleFileTransferStart(data, emitToChannel) {
151
233
  console.log(`Alerting client of incoming transfer from ${data.senderID}...`);
152
- const client = this.getClientFromID(data.recipientID);
234
+ const recipientConnectionID = FileNetClientManager.getOnlineClient(data.recipientID).id;
235
+ const client = this.getClientFromID(recipientConnectionID);
153
236
  if (!client) {
154
237
  return;
155
238
  }
@@ -175,11 +258,15 @@ class FileMessager {
175
258
  this.fileNet.emitToTide(connectionDetails.tideID, "current-online-clients", Object.fromEntries(FileNetClientManager.getOnlineClients()));
176
259
  return;
177
260
  }
178
- handleClientOnline(clientID, tideID, pClientID, connectionID) {
261
+ handleClientOnline(clientID, tideID, pClientID, connectionID, activeTransferBarrier) {
179
262
  if (!this.onlineClients.has(connectionID)) {
180
263
  return;
181
264
  }
182
265
  FileNetClientManager.addOnlineClient(clientID, pClientID, tideID, connectionID);
266
+ this.clientTransferBarriers.set(tideID, {
267
+ clientID,
268
+ activeTransferBarrier
269
+ });
183
270
  this.fileNet.emitToTide(tideID, "current-online-clients", Object.fromEntries(FileNetClientManager.getOnlineClients()));
184
271
  return;
185
272
  }
@@ -30,6 +30,7 @@ class FileMessagerClient {
30
30
  this.progressBarManager = new ConsoleProgressBarManager([], {
31
31
  completionMessageColor: chalk.blue.bold
32
32
  });
33
+ this.activeTransferTypes = new Map();
33
34
  this.clientTide = new HUDIoTide();
34
35
  this.clients = new Map();
35
36
  }
@@ -52,15 +53,56 @@ class FileMessagerClient {
52
53
  });
53
54
  });
54
55
  }
55
- requestTransfer(clientID, senderID, filePath, destinationPath) {
56
+ async requestTransfer(clientID, senderID, filePath, destinationPath, filterContent = []) {
57
+ const transferAllowed = await this.allowedTransfer(clientID, senderID, destinationPath, "N/A", "requestSend");
58
+ if (!transferAllowed) {
59
+ return;
60
+ }
56
61
  this.clientTide.emitEvent(clientID, "request-transfer-from-client", {
57
62
  requesterID: clientID,
58
63
  senderID,
59
64
  destinationPath,
65
+ filterContent,
60
66
  filePath
61
67
  });
62
68
  return;
63
69
  }
70
+ listFiles(clientID, recipientID, depth = 1) {
71
+ this.clientTide.emitEvent(clientID, "list-client-files", {
72
+ requesterID: clientID,
73
+ recipientID,
74
+ depth
75
+ });
76
+ return;
77
+ }
78
+ async allowedTransfer(clientID, recipientID, filePath, fileSize, transferType = "send") {
79
+ try {
80
+ const currentDate = Date.now();
81
+ const transferID = `${clientID}_${filePath}_${currentDate}`;
82
+ const transferAccepted = await this.clientTide.emitEventWithResponse(clientID, "get-client-transfer-barrier-response", "transfer-barrier-response", {
83
+ clientID,
84
+ recipientID,
85
+ transferID,
86
+ path: filePath,
87
+ date: currentDate,
88
+ transferType
89
+ }, 30000);
90
+ if (!transferAccepted) {
91
+ FileNetUtilityManager.outputGradient(`[FileTide ~ File Messager] ${recipientID} denied your transfer.`, errorMessageColors);
92
+ return false;
93
+ } else if (transferAccepted) {
94
+ FileNetUtilityManager.outputGradient(`[FileTide ~ File Messager] ${recipientID} accepted your transfer!`, significantMessageColors);
95
+ return true;
96
+ }
97
+ } catch (error) {
98
+ if (error.message.startsWith("Timeout: No response")) {
99
+ FileNetUtilityManager.outputGradient("[FileTide ~ File Messager] Transfer approval timed out.", errorMessageColors);
100
+ return false;
101
+ }
102
+ FileNetUtilityManager.outputGradient(`[FileTide ~ File Messager] Transfer approval refused.\n${error}`, errorMessageColors);
103
+ return false;
104
+ }
105
+ }
64
106
 
65
107
  /**
66
108
  * Sends all files in a directory in parallel using the sendFile method.
@@ -70,21 +112,33 @@ class FileMessagerClient {
70
112
  * @param {string} baseDirectory - The base directory path that contains the files.
71
113
  * @param {string} destinationPath - The path to which the files should be sent.
72
114
  */
73
- sendDirectoryFiles(clientID, recipientId, filesData, baseDirectory, destinationPath) {
115
+ async sendDirectoryFiles(clientID, recipientId, filesData, baseDirectory, destinationPath) {
74
116
  try {
75
117
  const baseDirectoryName = path.basename(baseDirectory);
76
118
  const adjustedDestinationPath = FileUtilityManager.normalizePath(path.join(destinationPath, baseDirectoryName));
77
- const sendFilePromises = filesData.map((fileInfo, index) => {
119
+ let totalSize = 0;
120
+ filesData.forEach(fileInfo => {
121
+ totalSize += fileInfo.size;
122
+ });
123
+ const incomingType = this.activeTransferTypes.get(destinationPath);
124
+ const transferAllowed = incomingType && incomingType === "requestedTransfer" ? true : await this.allowedTransfer(clientID, recipientId, adjustedDestinationPath, totalSize, "send");
125
+ if (incomingType) {
126
+ this.activeTransferTypes.delete(destinationPath);
127
+ }
128
+ if (!transferAllowed) {
129
+ return;
130
+ }
131
+ const sendFilePromises = filesData.map(async (fileInfo, index) => {
78
132
  const relativeFilePath = path.dirname(path.relative(baseDirectory, fileInfo.filePath));
79
133
  const fileDestinationPath = FileUtilityManager.normalizePath(path.join(adjustedDestinationPath, relativeFilePath));
80
- return this.sendFile(clientID, recipientId, fileInfo.fileName, fileInfo.fileData, fileDestinationPath, fileInfo.size, adjustedDestinationPath, index);
134
+ return await this.sendDirectoryFile(clientID, recipientId, fileInfo.fileName, fileInfo.fileData, fileDestinationPath, fileInfo.size, adjustedDestinationPath, index);
81
135
  });
82
136
  Promise.all(sendFilePromises).then(() => console.log("\nAll files in directory transferred successfully!")).catch(error => console.error("Did not transfer directory: ", error));
83
137
  } catch (error) {
84
138
  FileNetUtilityManager.outputGradient(`[FileTide ~ File Messager] ~ Refused directory transfer: ${error}`, errorMessageColors);
85
139
  }
86
140
  }
87
- sendFile(clientID, recipientId, fileName, file, filePath = process.cwd(), fileSize, directoryPath = null, index = 0) {
141
+ sendDirectoryFile(clientID, recipientId, fileName, file, filePath = process.cwd(), fileSize, directoryPath = null, index = 0) {
88
142
  if (!file) {
89
143
  return;
90
144
  }
@@ -146,6 +200,76 @@ class FileMessagerClient {
146
200
  }
147
201
  }).then(() => FileNetUtilityManager.outputGradient(`[FileTide ~ File Messager] ~ File ~ ${fileName} transfer complete!`, significantMessageColors)).catch(error => FileNetUtilityManager.outputGradient(`[FileTide ~ File Messager] ~ Refused file transfer: ${error}`, errorMessageColors));
148
202
  }
203
+ async sendFile(clientID, recipientId, fileName, file, filePath = process.cwd(), fileSize) {
204
+ if (!file) {
205
+ return;
206
+ }
207
+ const destinationPath = FileUtilityManager.normalizePath(path.join(filePath, fileName));
208
+ FileNetUtilityManager.outputGradient(`[FileTide ~ File Messager] ~ Requesting to transfer ~ ${destinationPath} to ${recipientId}!`, significantMessageColors);
209
+ const incomingType = this.activeTransferTypes.get(filePath);
210
+ const transferAllowed = incomingType && incomingType === "requestedTransfer" ? true : await this.allowedTransfer(clientID, recipientId, destinationPath, fileSize, "send");
211
+ if (incomingType) {
212
+ this.activeTransferTypes.delete(filePath);
213
+ }
214
+ if (!transferAllowed) {
215
+ return;
216
+ }
217
+ this.progressBarManager.completionMessage = `Successfully sent to ${recipientId}!`;
218
+ this.progressBarManager.displayPage("Page Number:", "Press 'n' for next page, 'p' for previous page.", chalk.blue, chalk.magenta);
219
+ this.progressBarManager.listenForInput("Page Number:", "Press 'n' for next page, 'p' for previous page.", chalk.blue, chalk.magenta, chalk.yellow);
220
+ this.clientTide.emitEvent(clientID, "transfer-start", {
221
+ fileName,
222
+ senderID: clientID,
223
+ recipientID: recipientId,
224
+ path: filePath
225
+ });
226
+ this.transporter.sendFile(file, {
227
+ onSendChunk: (transferId, chunkIndex, chunkData, totalChunks) => {
228
+ if (chunkIndex === 0) {
229
+ this.progressBarManager.addTask({
230
+ name: fileName,
231
+ size: fileSize
232
+ });
233
+ }
234
+ return new Promise((resolve, reject) => {
235
+ this.clientTide.emitEvent(clientID, "client-to-client-transfer", {
236
+ senderID: clientID,
237
+ recipientId,
238
+ fileName,
239
+ fileChunk: chunkData,
240
+ totalChunks,
241
+ chunkIndex,
242
+ filePath,
243
+ fileSize
244
+ });
245
+ resolve();
246
+ });
247
+ },
248
+ onComplete: () => {
249
+ return new Promise((resolve, reject) => {
250
+ this.clientTide.emitEvent(clientID, "transfer-complete", {
251
+ senderID: clientID,
252
+ recipientId,
253
+ fileName,
254
+ filePath
255
+ });
256
+ resolve();
257
+ });
258
+ },
259
+ onProgress: progress => {
260
+ this.progressBarManager.updateTaskProgress(fileName, 512 * 1024);
261
+ },
262
+ originDetails: {
263
+ recipientID: recipientId,
264
+ senderID: clientID
265
+ },
266
+ fileDetails: {
267
+ name: fileName,
268
+ path: filePath,
269
+ size: fileSize
270
+ }
271
+ }).then(() => FileNetUtilityManager.outputGradient(`[FileTide ~ File Messager] ~ File ~ ${fileName} transfer complete!`, significantMessageColors)).catch(error => FileNetUtilityManager.outputGradient(`[FileTide ~ File Messager] ~ Refused file transfer: ${error}`, errorMessageColors));
272
+ }
149
273
  }
150
274
  ;
151
275
  module.exports = FileMessagerClient;
@@ -34,6 +34,7 @@ class FileTide {
34
34
  completionMessageColor: chalk.blue.bold
35
35
  });
36
36
  this.clientShorePolicies = new Map();
37
+ this.transferBarrier = false;
37
38
  this.clients = new Map();
38
39
  this.currentUserID = "";
39
40
  return;
@@ -65,34 +66,51 @@ class FileTide {
65
66
  * @param {String} roomName - The name of the room to join
66
67
  * @param {String} userID - The ID of the client user
67
68
  * @param {Function} onLaunch - Callback for when a file messager launches
69
+ * @param {Function} onIncomingFile - Callback for incoming files in transit
70
+ * @param {Function} onIncomingTransfer - Callback for incoming transfers
71
+ * @param {Function} onTransferBarrier - Callback for when an incoming transfer status updates
72
+ * @param {Function} onTransferProgress - Callback for when an current transfers progress
73
+ * @param {Boolean} [enableTransferBarrier=true] - Whether to enable the transfer barrier
74
+ * @param {Object} [options = {}] - Additional options
68
75
  */
69
- launchMessager(clientOptions = {}, connectionOptions = {}, roomName, userID, onLaunch, onIncomingFile) {
76
+ launchMessager(clientOptions = {}, connectionOptions = {}, roomName, userID, onLaunch, onIncomingFile, onIncomingTransfer, onTransferBarrier, onTransferProgress, enableTransferBarrier = true, options = {}) {
70
77
  if (this.clients.has(userID)) {
71
78
  this.stopMessager(userID);
72
79
  }
73
80
  const newClient = new FileMessagerClient(clientOptions);
74
81
  newClient.joinRoom(userID, roomName, connectionOptions);
82
+ this.transferBarrier = enableTransferBarrier;
75
83
  this.clientShorePolicies.set(userID, [FileUtilityManager.getTidePath(">CurrentUser"), FileUtilityManager.getTidePath(">Downloads"), FileUtilityManager.getTidePath(">Documents")]);
76
84
  this.clients.set(userID, newClient);
77
85
  FileTide.outputGradient(`[FileTide] ~ Messager launched successfully for client ~ ${userID}!`, significantMessageColors);
78
- _assertClassBrand(_FileTide_brand, this, _setupFileEventListeners).call(this, userID, newClient, onIncomingFile);
86
+ _assertClassBrand(_FileTide_brand, this, _setupFileEventListeners).call(this, userID, newClient, onTransferProgress, onIncomingTransfer, onIncomingFile, onTransferBarrier, options);
79
87
  if (onLaunch) {
80
88
  onLaunch(newClient);
81
89
  }
82
90
  return;
83
91
  }
84
- getDirectoryData(directoryPath) {
85
- return FileUtilityManager.getDirectoryData(directoryPath);
92
+ async getDirectoryData(directoryPath) {
93
+ return await FileUtilityManager.getDirectoryData(directoryPath);
86
94
  }
87
95
  getFileData(filePath) {
88
96
  return FileUtilityManager.getFileData(filePath);
89
97
  }
90
- getPathData(inputPath) {
91
- return FileUtilityManager.getPathData(inputPath);
98
+ async getPathData(inputPath, filterContent = []) {
99
+ return await FileUtilityManager.getPathData(inputPath, filterContent);
92
100
  }
93
101
  getTidePath(inputPath) {
94
102
  return FileUtilityManager.getTidePath(inputPath);
95
103
  }
104
+ filterFilesAndDirectories(dirPath, filterContent = [], callback) {
105
+ FileUtilityManager.filterFilesAndDirectories(dirPath, filterContent, (files, error) => {
106
+ if (callback) {
107
+ callback(files, error);
108
+ }
109
+ });
110
+ }
111
+ async listFiles(directories, depth = 1) {
112
+ return await FileUtilityManager.listFiles(directories, depth);
113
+ }
96
114
  clearShorePolicies(clientID, shores = []) {
97
115
  let clientShorePolicies = this.clientShorePolicies.get(clientID);
98
116
  if (!shores || shores.length <= 0 || !clientShorePolicies || !clientID) {
@@ -122,15 +140,16 @@ class FileTide {
122
140
  * @param {String} recipientID - The ID of the client device to send the data to
123
141
  * @param {Buffer} fileData - The path of the data to send
124
142
  * @param {String} destinationPath - The destination path on the device
143
+ * @param {Array} filterDirectoryContent - List of paths to filter
125
144
  */
126
- sendToDevice(recipientID, filePath, destinationPath) {
127
- const fileDetails = this.getPathData(filePath);
145
+ async sendToDevice(recipientID, filePath, destinationPath, filterDirectoryContent = []) {
128
146
  const clientID = this.currentUserID;
129
147
  const clientMessager = this.clients.get(clientID);
130
148
  if (!clientMessager) {
131
149
  FileTide.outputGradient(`[FileTide] Client ~ ${clientID} not found.`, ["#F94144", "#F3722C"]);
132
150
  return;
133
151
  }
152
+ const fileDetails = await this.getPathData(filePath, filterDirectoryContent);
134
153
  if (!fileDetails) {
135
154
  FileTide.outputGradient(`[${clientID}] File at path ${filePath} not found.`, ["#F94144", "#F3722C"]);
136
155
  return;
@@ -203,7 +222,14 @@ class FileTide {
203
222
  }
204
223
  }
205
224
  _FileTide = FileTide;
206
- function _setupFileEventListeners(userID, client, onIncomingFile) {
225
+ function _setupFileEventListeners(userID, client, onTransferProgress, onIncomingTransfer, onIncomingFile, onTransferBarrier, options = {}) {
226
+ const {
227
+ onTransferStart,
228
+ onTransferComplete,
229
+ onTransferRequested,
230
+ onListFiles,
231
+ onCurrentOnlineClients
232
+ } = options;
207
233
  const cliTable = new ConsoleTable({
208
234
  padding: 2,
209
235
  headerAlign: "center",
@@ -244,10 +270,33 @@ function _setupFileEventListeners(userID, client, onIncomingFile) {
244
270
  cliTable.setTitle(`✨ Online >${Object.keys(currentClients).length}< ✨`, 1);
245
271
  cliTable.setData(currentClients);
246
272
  console.log(`\n[FileTide ~ FileNet]\n\n`, cliTable.render(), "\n");
273
+ if (onCurrentOnlineClients) {
274
+ onCurrentOnlineClients(currentClients);
275
+ }
276
+ });
277
+ client.clientTide.onEvent(userID, "queue-incoming-transfer", async data => {
278
+ if (this.transferBarrier && onIncomingTransfer) {
279
+ onIncomingTransfer(data, status => {
280
+ client.clientTide.emitEvent(userID, "notify-transfer-barrier", {
281
+ accepted: status,
282
+ data
283
+ });
284
+ });
285
+ return;
286
+ } else if (!this.transferBarrier) {
287
+ client.clientTide.emitEvent(userID, "notify-transfer-barrier", {
288
+ accepted: true,
289
+ data
290
+ });
291
+ }
292
+ return;
247
293
  });
248
- client.clientTide.onEvent(userID, "incoming-transfer", data => {
294
+ client.clientTide.onEvent(userID, "incoming-transfer", async data => {
249
295
  _FileTide.outputGradient(`[${userID}] Preparing to receive transfer : ${FileUtilityManager.getTidePath(data.path)}`, significantMessageColors);
250
- this.progressBarManager.completionMessage = `Successfully received transfer from ${recipientId}!`;
296
+ if (onTransferStart) {
297
+ onTransferStart(data);
298
+ }
299
+ this.progressBarManager.completionMessage = `Successfully received transfer from ${data.senderID}!`;
251
300
  this.progressBarManager.displayPage("Page Number:", "Press 'n' for next page, 'p' for previous page.", chalk.blue, chalk.magenta);
252
301
  this.progressBarManager.listenForInput("Page Number:", "Press 'n' for next page, 'p' for previous page.", chalk.blue, chalk.magenta, chalk.yellow);
253
302
  });
@@ -260,11 +309,17 @@ function _setupFileEventListeners(userID, client, onIncomingFile) {
260
309
  fileInfo: data
261
310
  });
262
311
  const saveDir = FileUtilityManager.getTidePath(data.path);
263
- if (data.senderID && data.senderID !== userID && !fs.existsSync(saveDir)) {
312
+ const verifiedSender = data.senderID && data.senderID !== userID;
313
+ if (verifiedSender) {
264
314
  this.progressBarManager.addTask({
265
315
  name: data.fileName,
266
316
  size: data.fileSize
267
317
  });
318
+ }
319
+ if (onIncomingFile) {
320
+ onIncomingFile(verifiedSender, data, activeTransfers);
321
+ }
322
+ if (verifiedSender && !fs.existsSync(saveDir)) {
268
323
  fs.mkdirSync(saveDir, {
269
324
  recursive: true
270
325
  });
@@ -274,6 +329,18 @@ function _setupFileEventListeners(userID, client, onIncomingFile) {
274
329
  _FileTide.outputGradient(`[${userID}] Halted file recieval preparation : ${data.fileName}\n${error}`, errorMessageColors);
275
330
  }
276
331
  });
332
+ client.clientTide.onEvent(userID, "accepted-transfer", async data => {
333
+ _FileTide.outputGradient(`[${userID}] Accepted ${data.clientID}'s transfer!`, significantMessageColors);
334
+ if (onTransferBarrier) {
335
+ onTransferBarrier(data);
336
+ }
337
+ });
338
+ client.clientTide.onEvent(userID, "denied-transfer", async data => {
339
+ _FileTide.outputGradient(`[${userID}] Denied ${data.clientID}'s transfer.`, errorMessageColors);
340
+ if (onTransferBarrier) {
341
+ onTransferBarrier(data);
342
+ }
343
+ });
277
344
  client.clientTide.onEvent(userID, "transfer-progress", data => {
278
345
  if (!data || data.fileChunk === undefined || data.fileChunk === null) {
279
346
  return;
@@ -285,8 +352,8 @@ function _setupFileEventListeners(userID, client, onIncomingFile) {
285
352
  transferState.receivedChunks[data.chunkIndex] = Buffer.from(data.fileChunk);
286
353
  const progress = (data.chunkIndex + 1) / transferState.totalChunks * 100;
287
354
  this.progressBarManager.updateTaskProgress(data.fileName, 512 * 1024);
288
- if (onIncomingFile) {
289
- onIncomingFile(data);
355
+ if (onTransferProgress) {
356
+ onTransferProgress(data);
290
357
  }
291
358
  });
292
359
  client.clientTide.onEvent(userID, "transfer-complete", data => {
@@ -301,14 +368,41 @@ function _setupFileEventListeners(userID, client, onIncomingFile) {
301
368
  fs.writeFileSync(savePath, fullFile);
302
369
  _FileTide.outputGradient(`[${userID}] File saved at : ${savePath}`, significantMessageColors);
303
370
  activeTransfers.delete(data.fileName);
371
+ if (onTransferComplete) {
372
+ onTransferComplete(data, activeTransfers, true);
373
+ }
304
374
  } catch (error) {
305
375
  _FileTide.outputGradient(`[${userID}] Did not save file at : ${savePath}\n${error}`, errorMessageColors);
306
376
  activeTransfers.delete(data.fileName);
377
+ if (onTransferComplete) {
378
+ onTransferComplete(data, activeTransfers, false);
379
+ }
307
380
  }
308
381
  });
309
- client.clientTide.onEvent(userID, "transfer-requested", data => {
382
+ client.clientTide.onEvent(userID, "transfer-requested", async data => {
310
383
  _FileTide.outputGradient(`[${userID}] Transfer requested from: ${data.requesterID}`, significantMessageColors);
311
- this.sendToDevice(data.requesterID, data.path, data.destinationPath);
384
+ this.clients.get(userID).activeTransferTypes.set(data.destinationPath, data.type);
385
+ if (onTransferRequested) {
386
+ onTransferRequested(data);
387
+ }
388
+ await this.sendToDevice(data.requesterID, data.path, data.destinationPath, data.filterContent || []);
389
+ return;
390
+ });
391
+ client.clientTide.onEvent(userID, "list-files", async data => {
392
+ _FileTide.outputGradient(`[${userID}] Sending file list!`, significantMessageColors);
393
+ const currentClientShorePolicies = this.getShorePolicies(userID);
394
+ const listedFiles = await this.listFiles(currentClientShorePolicies || [">Downloads"], data.depth || 1);
395
+ client.clientTide.emitEvent(userID, "send-file-list-to-client", {
396
+ senderID: data.recipientID,
397
+ recipientID: data.senderID,
398
+ listedFiles
399
+ });
400
+ });
401
+ client.clientTide.onEvent(userID, "client-file-list", async data => {
402
+ _FileTide.outputGradient(`[${userID}] Listed ${data.senderID}'s files!`, significantMessageColors);
403
+ if (onListFiles) {
404
+ onListFiles(data);
405
+ }
312
406
  });
313
407
  this.currentUserID = userID;
314
408
  return;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  const path = require("path");
4
+ const fsAsync = require("fs").promises;
4
5
  const fs = require("fs");
5
6
  const os = require("os");
6
7
  const filesPath = path.join(__dirname, "../files");
@@ -27,46 +28,169 @@ class FileUtilityManager {
27
28
 
28
29
  /**
29
30
  * Function to filter files and directories.
30
- * @param {string} dirPath - The directory path to scan
31
- * @param {Array} filterContent - Array of paths (files, directories, extensions) to avoid (e.g., ['docs', 'example.txt', '.js'])
32
- * @param {function} callback - A callback function to handle the results
31
+ * @param {string} dirPath - The directory path to scan.
32
+ * @param {Array} filterContent - Array of paths (files, directories, extensions) to avoid (e.g., ['docs', 'example.txt', '.js']).
33
+ * @returns {Promise<Array>} - A promise that resolves to an array of objects containing details about filtered files and directories.
33
34
  */
34
- static filterFilesAndDirectories(dirPath, filterContent = [], callback) {
35
- fs.readdir(dirPath, {
36
- withFileTypes: true
37
- }, (error, items) => {
38
- if (error) {
39
- return callback(error);
40
- }
35
+ static async filterFilesAndDirectories(dirPath, filterContent = []) {
36
+ try {
37
+ const currentDirPath = this.getTidePath(dirPath);
38
+ const items = await fs.promises.readdir(currentDirPath, {
39
+ withFileTypes: true
40
+ });
41
41
  const results = [];
42
- items.forEach(item => {
43
- const itemPath = path.join(dirPath, item.name);
42
+ for (const item of items) {
43
+ const itemPath = path.join(currentDirPath, item.name);
44
44
  const filtered = filterContent.some(filterEntry => {
45
- const resolvedFilterEntry = path.resolve(filterEntry);
45
+ const resolvedFilterEntry = path.resolve(currentDirPath, filterEntry);
46
46
  const fileExtension = filterEntry.startsWith(".");
47
47
  return itemPath === resolvedFilterEntry || item.name === filterEntry || fileExtension && path.extname(item.name) === filterEntry;
48
48
  });
49
49
  if (!filtered) {
50
- fs.stat(itemPath, (error, stats) => {
51
- if (error) {
52
- return callback(error);
53
- }
54
- results.push({
55
- dateCreated: stats.birthtime,
56
- type: item.isDirectory() ? "directory" : "file",
57
- size: stats.size,
58
- name: item.name,
59
- path: itemPath
60
- });
61
- if (results.length === items.length) {
62
- callback(null, results);
63
- }
50
+ const stats = await fs.promises.stat(itemPath);
51
+ results.push({
52
+ type: item.isDirectory() ? "directory" : "file",
53
+ dateCreated: stats.birthtime,
54
+ size: stats.size,
55
+ name: item.name,
56
+ path: itemPath
64
57
  });
65
- } else if (results.length === items.length) {
66
- callback(null, results);
67
58
  }
68
- });
59
+ }
60
+ return results;
61
+ } catch (error) {
62
+ console.error("Did not read directory: ", error);
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Function to list all paths across the system using wildcard '*'.
69
+ * @param {number} depth - The maximum depth limit (from the root). Defaults to 1.
70
+ * @returns {Promise<Array>} - A promise that resolves to an array of file and directory metadata across the system.
71
+ */
72
+ static async listAllPathsInOS(depth = 1) {
73
+ const rootPaths = ["/", "C:/"];
74
+ let allResults = [];
75
+ const rootPromises = rootPaths.map(async rootPath => {
76
+ if (await this.exists(rootPath)) {
77
+ const subResult = await this.listPaths(rootPath, depth);
78
+ allResults.push(...subResult.results);
79
+ }
80
+ });
81
+ await Promise.all(rootPromises);
82
+ return allResults;
83
+ }
84
+
85
+ /**
86
+ * Function to list all paths and their metadata recursively.
87
+ * @param {string} dir - The directory path to scan.
88
+ * @param {number} maxDepth - The maximum depth limit (from the root). Defaults to 1.
89
+ * @param {number} currentDepth - The current depth level. Starts at 0 (for the root).
90
+ * @returns {Promise<object>} - A promise that resolves to an object containing a list of files/directories with metadata.
91
+ */
92
+ static async listPaths(dir, maxDepth = 1, currentDepth = 0) {
93
+ let results = [];
94
+ if (!maxDepth || currentDepth === undefined || currentDepth === null || currentDepth > maxDepth) {
95
+ return {
96
+ totalItems: 0,
97
+ totalSize: 0,
98
+ results
99
+ };
100
+ }
101
+ const list = await fsAsync.readdir(dir, {
102
+ withFileTypes: true
103
+ });
104
+ const stats = await fsAsync.stat(dir);
105
+ const dirMetadata = {
106
+ totalItems: 0,
107
+ totalSize: 0,
108
+ createdTime: stats.birthtime,
109
+ lastModified: stats.mtime,
110
+ type: "directory",
111
+ path: dir
112
+ };
113
+ results.push(dirMetadata);
114
+ const promises = list.map(async file => {
115
+ const filePath = this.getTidePath(path.join(dir, file.name));
116
+ try {
117
+ if (file.isDirectory()) {
118
+ const subDirResult = await this.listPaths(filePath, maxDepth, currentDepth + 1);
119
+ dirMetadata.totalItems += subDirResult.totalItems;
120
+ dirMetadata.totalSize += subDirResult.totalSize;
121
+ results.push(...subDirResult.results);
122
+ } else {
123
+ const fileStats = await fsAsync.stat(filePath);
124
+ const fileMetadata = {
125
+ createdTime: fileStats.birthtime,
126
+ lastModified: fileStats.mtime,
127
+ size: fileStats.size,
128
+ path: filePath,
129
+ type: "file"
130
+ };
131
+ dirMetadata.totalSize += fileStats.size;
132
+ dirMetadata.totalItems += 1;
133
+ results.push(fileMetadata);
134
+ }
135
+ } catch (error) {
136
+ console.log(`[FileTide] Path not found: ${filePath}`);
137
+ }
69
138
  });
139
+ await Promise.all(promises);
140
+ return {
141
+ totalItems: dirMetadata.totalItems,
142
+ totalSize: dirMetadata.totalSize,
143
+ results
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Allows for listing files in a specific directory or all files in the system '*'.
149
+ * @param {Array} directories - Array of directories to scan or '*' for entire system.
150
+ * @param {number} depth - The maximum depth limit (from the root). Defaults to 1.
151
+ * @returns {Promise<Array>} - A promise that resolves to an array of file and directory metadata from specified directories or the entire system.
152
+ */
153
+ static async listFiles(directories, depth = 1) {
154
+ let fileList = [];
155
+ if (directories.includes("*")) {
156
+ fileList = await this.listAllPathsInOS(depth);
157
+ } else {
158
+ const dirPromises = directories.map(async dir => {
159
+ const currentDir = this.getTidePath(dir);
160
+ if (await this.exists(currentDir)) {
161
+ const subResult = await this.listPaths(currentDir, depth);
162
+ fileList.push(...subResult.results);
163
+ } else {
164
+ console.warn(`Directory does not exist: ${currentDir}`);
165
+ }
166
+ });
167
+ await Promise.all(dirPromises);
168
+ }
169
+ return fileList;
170
+ }
171
+
172
+ /**
173
+ * Checks the path's type.
174
+ * @param {string} inputPath - The path to check.
175
+ * @returns {Promise<boolean>} - Returns a promise that resolves to true if a directory, false otherwise.
176
+ */
177
+ static async directoryPath(inputPath) {
178
+ const stats = await fsAsync.stat(inputPath);
179
+ return stats.isDirectory();
180
+ }
181
+
182
+ /**
183
+ * Helper function to check if a path exists.
184
+ * @param {string} inputPath - The path to check.
185
+ * @returns {Promise<boolean>} - Returns a promise that resolves to true if the path exists, false otherwise.
186
+ */
187
+ static async exists(inputPath) {
188
+ try {
189
+ await fsAsync.access(inputPath);
190
+ return true;
191
+ } catch (e) {
192
+ return false;
193
+ }
70
194
  }
71
195
 
72
196
  /**
@@ -153,31 +277,31 @@ class FileUtilityManager {
153
277
  /**
154
278
  * Gets all files' data from a given directory.
155
279
  * @param {string} dirPath - The path to the directory.
280
+ * @param {Array} filterContent - The paths to filter from the directory.
156
281
  * @returns {Array} - An array of objects containing file data, name, and path.
157
282
  */
158
- static getDirectoryData(inputDirPath) {
283
+ static async getDirectoryData(inputDirPath, filterContent = []) {
159
284
  const dirPath = this.getTidePath(inputDirPath);
160
285
  if (!fs.existsSync(dirPath)) {
161
286
  console.log(`Directory at path ${dirPath} not found.`);
162
287
  return [];
163
288
  }
164
289
  const filesData = [];
165
- function readDirectory(currentPath) {
166
- const fileList = fs.readdirSync(currentPath);
167
- fileList.forEach(file => {
168
- const filePath = FileUtilityManager.normalizePath(path.join(currentPath, file));
169
- const stats = fs.statSync(filePath);
290
+ async function readDirectory(currentPath) {
291
+ const files = await FileUtilityManager.filterFilesAndDirectories(currentPath, filterContent);
292
+ for (const file of files) {
293
+ const stats = await fs.promises.stat(file.path);
170
294
  if (stats.isFile()) {
171
- const fileData = FileUtilityManager.getFileData(filePath);
295
+ const fileData = FileUtilityManager.getFileData(file.path);
172
296
  if (fileData) {
173
297
  filesData.push(fileData);
174
298
  }
175
299
  } else if (stats.isDirectory()) {
176
- readDirectory(filePath);
300
+ await readDirectory(file.path);
177
301
  }
178
- });
302
+ }
179
303
  }
180
- readDirectory(dirPath);
304
+ await readDirectory(dirPath);
181
305
  return filesData;
182
306
  }
183
307
 
@@ -207,9 +331,10 @@ class FileUtilityManager {
207
331
  /**
208
332
  * Gets data from a file or directory based on the provided path.
209
333
  * @param {string} inputPath - The path to a file or directory.
334
+ * @param {Array} filterContent - The paths to filter from the directory if getting a directory.
210
335
  * @returns {Object|Array|null} - File data if it's a file, an array of file data if it's a directory, or null if the path doesn't exist.
211
336
  */
212
- static getPathData(inputPath) {
337
+ static async getPathData(inputPath, filterContent = []) {
213
338
  const currentInputPath = this.getTidePath(inputPath);
214
339
  if (!fs.existsSync(currentInputPath)) {
215
340
  console.log(`Path at ${currentInputPath} not found.`);
@@ -223,7 +348,7 @@ class FileUtilityManager {
223
348
  };
224
349
  } else if (stats.isDirectory()) {
225
350
  return {
226
- content: this.getDirectoryData(currentInputPath),
351
+ content: await this.getDirectoryData(currentInputPath, filterContent),
227
352
  type: "directory"
228
353
  };
229
354
  }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+
3
+ class HUDTargetQueue {
4
+ constructor() {
5
+ this.queues = new Map();
6
+ }
7
+
8
+ /**
9
+ * Adds a function to the queue for a specific ID and target value.
10
+ * @param {string} id - A unique identifier for the value being watched.
11
+ * @param {function} fn - The function to execute when the target value is reached.
12
+ */
13
+ enqueue(id, fn) {
14
+ if (!this.queues.has(id)) {
15
+ this.queues.set(id, []);
16
+ }
17
+ this.queues.get(id).push(fn);
18
+ }
19
+
20
+ /**
21
+ * Waits for a value to change to the target value and executes only the functions
22
+ * in the queue associated with the specific ID.
23
+ * @param {string} id - A unique identifier for the value being watched.
24
+ * @param {function} getValue - A function that returns the current value.
25
+ * @param {any|array} targetValues - The value(s) to wait for.
26
+ * @param {number} interval - The interval (in milliseconds) to check the value.
27
+ * @param {number} [timeout] - Optional. The maximum time (in milliseconds) to wait before rejecting.
28
+ * @returns {Promise} - Resolves when the value matches the target value, executes queued functions.
29
+ */
30
+ waitForData(id, getValue, targetValues, interval = 100, timeout) {
31
+ return new Promise((resolve, reject) => {
32
+ let elapsedTime = 0;
33
+ const targets = Array.isArray(targetValues) ? targetValues : [targetValues];
34
+ const checkData = async () => {
35
+ const currentValue = await getValue();
36
+ if (typeof currentValue === "object" && targets.some(target => typeof target === "object")) {
37
+ if (targets.some(target => JSON.stringify(currentValue) === JSON.stringify(target))) {
38
+ this.executeQueue(id);
39
+ resolve(currentValue);
40
+ }
41
+ } else if (targets.includes(currentValue)) {
42
+ this.executeQueue(id);
43
+ resolve(currentValue);
44
+ } else if (timeout && elapsedTime >= timeout) {
45
+ reject(new Error(`Timeout: Value did not reach target within ${timeout}ms`));
46
+ } else {
47
+ elapsedTime += interval;
48
+ setTimeout(checkData, interval);
49
+ }
50
+ };
51
+ checkData();
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Executes all functions in the queue associated with the specific ID.
57
+ * @param {string} id - A unique identifier for the value being watched.
58
+ */
59
+ executeQueue(id) {
60
+ const queue = this.queues.get(id);
61
+ if (queue) {
62
+ while (queue.length > 0) {
63
+ const fn = queue.shift();
64
+ if (typeof fn === "function") {
65
+ fn();
66
+ }
67
+ }
68
+ this.queues.delete(id);
69
+ }
70
+ }
71
+ }
72
+ module.exports = HUDTargetQueue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trap_stevo/filetide",
3
- "version": "0.0.32",
3
+ "version": "0.0.34",
4
4
  "description": "Revolutionizing real-time file transfer with seamless, instant communication across any device. Deliver files instantly, regardless of platform, and experience unparalleled speed and control in managing transfers. Elevate your file-sharing capabilities with a tool designed for precision, efficiency, and effortless connectivity.",
5
5
  "main": "dist/cjs/FileTide.js",
6
6
  "scripts": {
@@ -27,10 +27,10 @@
27
27
  "seamless connectivity"
28
28
  ],
29
29
  "author": "Steven Compton",
30
- "license": "ISC",
30
+ "license": "See License in LICENSE.md",
31
31
  "dependencies": {
32
32
  "@trap_stevo/iotide": "^0.0.37",
33
- "@trap_stevo/iotide-client": "^0.0.15",
33
+ "@trap_stevo/iotide-client": "^0.0.20",
34
34
  "@trap_stevo/legendarybuilderpronodejs-utilities": "^1.0.40",
35
35
  "chalk": "^4.1.2",
36
36
  "readline": "^1.3.0"