Vu Minh Chien commited on
Commit
8896972
Β·
1 Parent(s): 758d55d

Implement HF Dataset for persistent device storage

Browse files

- Add HFDatasetManager for HF dataset operations
- Replace file-based storage with HF dataset
- Add automatic dataset creation and management
- Implement fallback to in-memory storage
- Update all device save/load operations
- Add comprehensive error handling
- Include setup documentation

Files changed (4) hide show
  1. HF-DATASET-SETUP.md +169 -0
  2. hf-dataset.js +177 -0
  3. package.json +3 -1
  4. server.js +40 -47
HF-DATASET-SETUP.md ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # πŸ€— Hugging Face Dataset Setup Guide
2
+
3
+ ## Overview
4
+
5
+ This server now uses **Hugging Face Dataset** for persistent storage of device tokens instead of local files. This ensures that device data is preserved across Space restarts and provides better data management.
6
+
7
+ ## πŸš€ Quick Setup
8
+
9
+ ### 1. Generate Hugging Face Token
10
+
11
+ 1. Go to [Hugging Face Settings](https://huggingface.co/settings/tokens)
12
+ 2. Click "Create new token"
13
+ 3. Name: `houzou-notification-server`
14
+ 4. Type: **Write** (required for dataset operations)
15
+ 5. Copy the token
16
+
17
+ ### 2. Configure Space Secrets
18
+
19
+ 1. Go to your Space Settings β†’ **Secrets and variables**
20
+ 2. Add these secrets:
21
+
22
+ ```
23
+ HF_TOKEN = hf_your_token_here
24
+ HF_DATASET_ID = Detomo/houzou-devices (optional, defaults to this)
25
+ ```
26
+
27
+ ### 3. Required Secrets Summary
28
+
29
+ Your Space needs these secrets:
30
+
31
+ ```
32
+ FIREBASE_SERVICE_ACCOUNT = {your firebase config JSON}
33
+ HF_TOKEN = hf_your_write_token_here
34
+ HF_DATASET_ID = Detomo/houzou-devices (optional)
35
+ ```
36
+
37
+ ## πŸ“Š Dataset Structure
38
+
39
+ The dataset will be automatically created with:
40
+
41
+ ```
42
+ datasets/Detomo/houzou-devices/
43
+ β”œβ”€β”€ README.md # Dataset documentation
44
+ └── devices.json # Device tokens and metadata
45
+ ```
46
+
47
+ ### devices.json Format:
48
+ ```json
49
+ [
50
+ [
51
+ "device-id-1",
52
+ {
53
+ "token": "fcm_token_here",
54
+ "lastUpdated": "2024-01-01T00:00:00.000Z",
55
+ "platform": "iOS",
56
+ "appVersion": "1.0.0",
57
+ "registered": true
58
+ }
59
+ ]
60
+ ]
61
+ ```
62
+
63
+ ## πŸ”§ Features
64
+
65
+ ### βœ… Automatic Dataset Management
66
+ - Creates dataset if it doesn't exist
67
+ - Handles permissions automatically
68
+ - Provides fallback to in-memory storage
69
+
70
+ ### βœ… Persistent Storage
71
+ - Device tokens survive Space restarts
72
+ - Automatic backup to HF dataset
73
+ - Version control for device data
74
+
75
+ ### βœ… Error Handling
76
+ - Graceful fallback if HF API is unavailable
77
+ - Detailed logging for troubleshooting
78
+ - Continues operation even if dataset access fails
79
+
80
+ ## πŸ” Verification
81
+
82
+ ### Check Dataset Status
83
+ ```bash
84
+ curl https://your-space-url.hf.space/
85
+ ```
86
+
87
+ Response includes:
88
+ ```json
89
+ {
90
+ "message": "Houzou Medical Notification Server",
91
+ "status": "running",
92
+ "hfDataset": {
93
+ "enabled": true,
94
+ "datasetId": "Detomo/houzou-devices",
95
+ "hasToken": true
96
+ },
97
+ "deviceCount": 5
98
+ }
99
+ ```
100
+
101
+ ### View Dataset
102
+ 1. Go to: https://huggingface.co/datasets/Detomo/houzou-devices
103
+ 2. Check `devices.json` for your device data
104
+
105
+ ## πŸ› Troubleshooting
106
+
107
+ ### Dataset Not Created
108
+ - Verify `HF_TOKEN` has **Write** permissions
109
+ - Check Space logs for error messages
110
+ - Ensure token is valid and not expired
111
+
112
+ ### Data Not Saving
113
+ - Check if HF API is responding
114
+ - Verify dataset permissions
115
+ - Server will continue with in-memory storage as fallback
116
+
117
+ ### Common Errors
118
+
119
+ **403 Forbidden**
120
+ - Token doesn't have write permissions
121
+ - Token is expired or invalid
122
+
123
+ **404 Not Found**
124
+ - Dataset doesn't exist (will be auto-created)
125
+ - Wrong dataset ID
126
+
127
+ **Rate Limiting**
128
+ - HF API has rate limits
129
+ - Server will retry automatically
130
+
131
+ ## πŸ”„ Migration from File Storage
132
+
133
+ If you're migrating from file-based storage:
134
+
135
+ 1. **Backup existing data**: Download your current `devices.json`
136
+ 2. **Set up HF dataset**: Follow steps above
137
+ 3. **Deploy updated server**: The server will automatically use HF dataset
138
+ 4. **Verify**: Check that devices are loaded correctly
139
+
140
+ ## πŸ“‹ Environment Variables
141
+
142
+ ```bash
143
+ # Required
144
+ HF_TOKEN=hf_your_write_token_here
145
+ FIREBASE_SERVICE_ACCOUNT={"type":"service_account",...}
146
+
147
+ # Optional
148
+ HF_DATASET_ID=Detomo/houzou-devices
149
+ PORT=7860
150
+ NODE_ENV=production
151
+ ```
152
+
153
+ ## 🎯 Benefits
154
+
155
+ - **βœ… Persistent**: Data survives Space restarts
156
+ - **βœ… Reliable**: Automatic fallback mechanisms
157
+ - **βœ… Scalable**: No file permission issues
158
+ - **βœ… Traceable**: Version history in dataset
159
+ - **βœ… Secure**: Tokens stored in HF infrastructure
160
+
161
+ ## πŸ”— Links
162
+
163
+ - [Hugging Face Hub API](https://huggingface.co/docs/hub/api)
164
+ - [Dataset Management](https://huggingface.co/docs/datasets/)
165
+ - [Access Tokens](https://huggingface.co/settings/tokens)
166
+
167
+ ---
168
+
169
+ **Ready to deploy!** Your notification server will now use HF Dataset for persistent storage. πŸš€
hf-dataset.js ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const { HfApi } = require("@huggingface/hub");
2
+ const fetch = require('node-fetch');
3
+
4
+ class HFDatasetManager {
5
+ constructor() {
6
+ this.hfToken = process.env.HF_TOKEN;
7
+ this.datasetId = process.env.HF_DATASET_ID || "Detomo/houzou-devices";
8
+ this.fileName = "devices.json";
9
+ this.hfApi = new HfApi({ accessToken: this.hfToken });
10
+ this.isEnabled = !!this.hfToken;
11
+
12
+ console.log(`πŸ€— HF Dataset Manager initialized`);
13
+ console.log(` Dataset: ${this.datasetId}`);
14
+ console.log(` Enabled: ${this.isEnabled}`);
15
+ }
16
+
17
+ async loadDevices() {
18
+ if (!this.isEnabled) {
19
+ console.log('⚠️ HF Dataset disabled - no token provided');
20
+ return new Map();
21
+ }
22
+
23
+ try {
24
+ console.log('πŸ“₯ Loading devices from HF dataset...');
25
+
26
+ const fileUrl = `https://huggingface.co/datasets/${this.datasetId}/resolve/main/${this.fileName}`;
27
+ const response = await fetch(fileUrl, {
28
+ headers: {
29
+ 'Authorization': `Bearer ${this.hfToken}`
30
+ }
31
+ });
32
+
33
+ if (response.status === 404) {
34
+ console.log('πŸ“ No devices file found in dataset, creating new one');
35
+ await this.saveDevices(new Map());
36
+ return new Map();
37
+ }
38
+
39
+ if (!response.ok) {
40
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
41
+ }
42
+
43
+ const data = await response.json();
44
+ const deviceMap = new Map(data);
45
+
46
+ console.log(`πŸ“₯ Loaded ${deviceMap.size} devices from HF dataset`);
47
+ return deviceMap;
48
+
49
+ } catch (error) {
50
+ console.error('❌ Error loading devices from HF dataset:', error);
51
+ console.log('⚠️ Falling back to empty device list');
52
+ return new Map();
53
+ }
54
+ }
55
+
56
+ async saveDevices(deviceMap) {
57
+ if (!this.isEnabled) {
58
+ console.log('⚠️ HF Dataset disabled - cannot save devices');
59
+ return false;
60
+ }
61
+
62
+ try {
63
+ console.log(`πŸ’Ύ Saving ${deviceMap.size} devices to HF dataset...`);
64
+
65
+ const devicesArray = Array.from(deviceMap.entries());
66
+ const jsonData = JSON.stringify(devicesArray, null, 2);
67
+
68
+ // Create a blob from the JSON data
69
+ const blob = new Blob([jsonData], { type: 'application/json' });
70
+
71
+ // Upload to HF dataset
72
+ await this.hfApi.uploadFile({
73
+ repo: this.datasetId,
74
+ file: {
75
+ path: this.fileName,
76
+ content: blob
77
+ },
78
+ commitMessage: `Update devices data - ${new Date().toISOString()}`,
79
+ repoType: "dataset"
80
+ });
81
+
82
+ console.log(`βœ… Successfully saved ${deviceMap.size} devices to HF dataset`);
83
+ return true;
84
+
85
+ } catch (error) {
86
+ console.error('❌ Error saving devices to HF dataset:', error);
87
+ return false;
88
+ }
89
+ }
90
+
91
+ async createDatasetIfNotExists() {
92
+ if (!this.isEnabled) {
93
+ console.log('⚠️ HF Dataset disabled - cannot create dataset');
94
+ return false;
95
+ }
96
+
97
+ try {
98
+ console.log('πŸ” Checking if dataset exists...');
99
+
100
+ // Try to get dataset info
101
+ const datasetInfo = await this.hfApi.datasetInfo({
102
+ repo: this.datasetId
103
+ });
104
+
105
+ console.log('βœ… Dataset already exists');
106
+ return true;
107
+
108
+ } catch (error) {
109
+ if (error.statusCode === 404) {
110
+ console.log('πŸ“ Dataset not found, creating new one...');
111
+
112
+ try {
113
+ // Create the dataset
114
+ await this.hfApi.createRepo({
115
+ repo: this.datasetId,
116
+ type: "dataset",
117
+ private: false
118
+ });
119
+
120
+ console.log('βœ… Dataset created successfully');
121
+
122
+ // Create initial README
123
+ const readmeContent = `# Houzou Medical Devices Dataset
124
+
125
+ This dataset stores FCM tokens and device information for the Houzou Medical app notification system.
126
+
127
+ ## Files
128
+
129
+ - \`devices.json\`: Contains device tokens and metadata
130
+
131
+ ## Usage
132
+
133
+ This dataset is automatically managed by the Houzou Medical Notification Server.
134
+
135
+ Last updated: ${new Date().toISOString()}
136
+ `;
137
+
138
+ await this.hfApi.uploadFile({
139
+ repo: this.datasetId,
140
+ file: {
141
+ path: "README.md",
142
+ content: new Blob([readmeContent], { type: 'text/markdown' })
143
+ },
144
+ commitMessage: "Initial dataset setup",
145
+ repoType: "dataset"
146
+ });
147
+
148
+ // Create initial empty devices file
149
+ await this.saveDevices(new Map());
150
+
151
+ return true;
152
+
153
+ } catch (createError) {
154
+ console.error('❌ Error creating dataset:', createError);
155
+ return false;
156
+ }
157
+ } else {
158
+ console.error('❌ Error checking dataset:', error);
159
+ return false;
160
+ }
161
+ }
162
+ }
163
+
164
+ isReady() {
165
+ return this.isEnabled;
166
+ }
167
+
168
+ getStatus() {
169
+ return {
170
+ enabled: this.isEnabled,
171
+ datasetId: this.datasetId,
172
+ hasToken: !!this.hfToken
173
+ };
174
+ }
175
+ }
176
+
177
+ module.exports = HFDatasetManager;
package.json CHANGED
@@ -12,7 +12,9 @@
12
  "firebase-admin": "^12.0.0",
13
  "cors": "^2.8.5",
14
  "body-parser": "^1.20.2",
15
- "dotenv": "^16.3.1"
 
 
16
  },
17
  "devDependencies": {
18
  "nodemon": "^3.0.2"
 
12
  "firebase-admin": "^12.0.0",
13
  "cors": "^2.8.5",
14
  "body-parser": "^1.20.2",
15
+ "dotenv": "^16.3.1",
16
+ "@huggingface/hub": "^0.15.1",
17
+ "node-fetch": "^3.3.2"
18
  },
19
  "devDependencies": {
20
  "nodemon": "^3.0.2"
server.js CHANGED
@@ -3,6 +3,7 @@ const admin = require('firebase-admin');
3
  const cors = require('cors');
4
  const bodyParser = require('body-parser');
5
  const os = require('os');
 
6
  require('dotenv').config();
7
 
8
  const app = express();
@@ -59,60 +60,52 @@ const sampleProducts = [
59
  const fs = require('fs');
60
  const path = require('path');
61
 
62
- // File storage for FCM tokens
63
- const DEVICES_FILE = path.join(__dirname, 'devices.json');
64
 
65
- // Fallback to in-memory storage if file operations fail
66
- let fileOperationsEnabled = true;
67
-
68
- // Load devices from file or create empty storage
69
  let deviceTokens = new Map();
70
 
71
- function loadDevicesFromFile() {
72
  try {
73
- if (fs.existsSync(DEVICES_FILE)) {
74
- const data = fs.readFileSync(DEVICES_FILE, 'utf8');
75
- const devicesArray = JSON.parse(data);
76
- deviceTokens = new Map(devicesArray);
77
- console.log(`πŸ“ Loaded ${deviceTokens.size} devices from file`);
78
- } else {
79
- console.log('πŸ“ No devices file found, starting fresh');
80
- // Try to create an empty file to test write permissions
81
- try {
82
- fs.writeFileSync(DEVICES_FILE, '[]');
83
- console.log('πŸ“ Created empty devices file');
84
- } catch (writeError) {
85
- console.warn('⚠️ Cannot create devices file, will use in-memory storage only');
86
- fileOperationsEnabled = false;
87
- }
88
- }
89
  } catch (error) {
90
- console.error('❌ Error loading devices file:', error);
91
- console.log('⚠️ Using in-memory storage only');
92
- fileOperationsEnabled = false;
93
  deviceTokens = new Map();
94
  }
95
  }
96
 
97
- function saveDevicesToFile() {
98
- if (!fileOperationsEnabled) {
99
- console.log('πŸ’Ύ File operations disabled, keeping devices in memory only');
100
- return;
 
 
 
 
 
 
101
  }
102
-
 
 
 
103
  try {
104
- const devicesArray = Array.from(deviceTokens.entries());
105
- fs.writeFileSync(DEVICES_FILE, JSON.stringify(devicesArray, null, 2));
106
- console.log(`πŸ’Ύ Saved ${deviceTokens.size} devices to file`);
 
 
 
 
107
  } catch (error) {
108
- console.error('❌ Error saving devices file:', error);
109
- console.log('⚠️ Disabling file operations, using in-memory storage only');
110
- fileOperationsEnabled = false;
111
  }
112
  }
113
 
114
- // Load devices on startup
115
- loadDevicesFromFile();
116
 
117
  // Routes
118
 
@@ -122,7 +115,7 @@ app.get('/', (req, res) => {
122
  message: 'Houzou Medical Notification Server',
123
  status: 'running',
124
  timestamp: new Date().toISOString(),
125
- fileOperationsEnabled: fileOperationsEnabled,
126
  deviceCount: deviceTokens.size
127
  });
128
  });
@@ -433,8 +426,8 @@ app.post('/register-token', async (req, res) => {
433
  console.warn(`⚠️ Failed to subscribe device to topic: ${topicError.message}`);
434
  }
435
 
436
- // Save to file
437
- saveDevicesToFile();
438
 
439
  res.json({
440
  success: true,
@@ -476,8 +469,8 @@ app.post('/unregister-token', async (req, res) => {
476
  console.log(`πŸ“± Token unregistered for device ${deviceId}`);
477
  }
478
 
479
- // Save to file
480
- saveDevicesToFile();
481
 
482
  res.json({
483
  success: true,
@@ -531,7 +524,7 @@ app.post('/debug-add-device', (req, res) => {
531
  registered: true
532
  });
533
 
534
- saveDevicesToFile();
535
 
536
  console.log(`πŸ”§ Debug: Added device ${deviceId} (${platform})`);
537
 
@@ -642,7 +635,7 @@ app.post('/send-broadcast-notification', async (req, res) => {
642
  }
643
  }
644
  });
645
- saveDevicesToFile();
646
  } else {
647
  console.log(`⚠️ No tokens removed - all failures appear to be temporary`);
648
  }
@@ -701,7 +694,7 @@ app.listen(PORT, () => {
701
  console.log(` Local: http://localhost:${PORT}`);
702
  console.log(` Network: http://${localIP}:${PORT}`);
703
  console.log(`\nπŸ”§ For iPhone app, use: http://${localIP}:${PORT}`);
704
- console.log(`πŸ“ Devices saved to: ${DEVICES_FILE}`);
705
  });
706
 
707
  module.exports = app;
 
3
  const cors = require('cors');
4
  const bodyParser = require('body-parser');
5
  const os = require('os');
6
+ const HFDatasetManager = require('./hf-dataset');
7
  require('dotenv').config();
8
 
9
  const app = express();
 
60
  const fs = require('fs');
61
  const path = require('path');
62
 
63
+ // Initialize HF Dataset Manager
64
+ const hfDataset = new HFDatasetManager();
65
 
66
+ // Device storage
 
 
 
67
  let deviceTokens = new Map();
68
 
69
+ async function loadDevicesFromHF() {
70
  try {
71
+ deviceTokens = await hfDataset.loadDevices();
72
+ console.log(`πŸ“₯ Loaded ${deviceTokens.size} devices from HF dataset`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  } catch (error) {
74
+ console.error('❌ Error loading devices from HF dataset:', error);
 
 
75
  deviceTokens = new Map();
76
  }
77
  }
78
 
79
+ async function saveDevicesToHF() {
80
+ try {
81
+ const success = await hfDataset.saveDevices(deviceTokens);
82
+ if (success) {
83
+ console.log(`πŸ’Ύ Saved ${deviceTokens.size} devices to HF dataset`);
84
+ } else {
85
+ console.log('⚠️ Failed to save devices to HF dataset');
86
+ }
87
+ } catch (error) {
88
+ console.error('❌ Error saving devices to HF dataset:', error);
89
  }
90
+ }
91
+
92
+ // Initialize HF dataset and load devices on startup
93
+ async function initializeServer() {
94
  try {
95
+ // Create dataset if it doesn't exist
96
+ await hfDataset.createDatasetIfNotExists();
97
+
98
+ // Load devices from HF dataset
99
+ await loadDevicesFromHF();
100
+
101
+ console.log('βœ… Server initialization completed');
102
  } catch (error) {
103
+ console.error('❌ Error during server initialization:', error);
 
 
104
  }
105
  }
106
 
107
+ // Start initialization (don't await here to avoid blocking)
108
+ initializeServer();
109
 
110
  // Routes
111
 
 
115
  message: 'Houzou Medical Notification Server',
116
  status: 'running',
117
  timestamp: new Date().toISOString(),
118
+ hfDataset: hfDataset.getStatus(),
119
  deviceCount: deviceTokens.size
120
  });
121
  });
 
426
  console.warn(`⚠️ Failed to subscribe device to topic: ${topicError.message}`);
427
  }
428
 
429
+ // Save to HF dataset
430
+ saveDevicesToHF();
431
 
432
  res.json({
433
  success: true,
 
469
  console.log(`πŸ“± Token unregistered for device ${deviceId}`);
470
  }
471
 
472
+ // Save to HF dataset
473
+ saveDevicesToHF();
474
 
475
  res.json({
476
  success: true,
 
524
  registered: true
525
  });
526
 
527
+ saveDevicesToHF();
528
 
529
  console.log(`πŸ”§ Debug: Added device ${deviceId} (${platform})`);
530
 
 
635
  }
636
  }
637
  });
638
+ saveDevicesToHF();
639
  } else {
640
  console.log(`⚠️ No tokens removed - all failures appear to be temporary`);
641
  }
 
694
  console.log(` Local: http://localhost:${PORT}`);
695
  console.log(` Network: http://${localIP}:${PORT}`);
696
  console.log(`\nπŸ”§ For iPhone app, use: http://${localIP}:${PORT}`);
697
+ console.log(`πŸ€— Devices stored in HF dataset: ${hfDataset.datasetId}`);
698
  });
699
 
700
  module.exports = app;