fazeel007 commited on
Commit
ccf1a85
Β·
1 Parent(s): 484ea5f

Fix Hugging Face Spaces deployment: Handle file permissions and production environment

Browse files

πŸ”§ Production File Permissions:
- Use /tmp directory for uploads and database in production (NODE_ENV=production)
- Add error handling for mkdir operations that fail due to permissions
- Graceful fallback when directory creation fails

πŸš€ Hugging Face Spaces Compatibility:
- Detect production environment and use writable /tmp paths
- Added ENABLE_UPLOADS environment variable to control file upload functionality
- Fallback routes for disabled uploads with helpful error messages

πŸ’Ύ Database & Storage Fixes:
- SQLite database path: /tmp/knowledgebridge.db in production
- Upload directory: /tmp/uploads in production vs ./uploads in development
- Error handling for permission denied scenarios

🎯 Graceful Degradation:
- App continues to work even when file operations fail
- Clear error messages explaining disabled functionality
- Traditional search and external sources still available

πŸ”’ Environment Detection:
- Automatic detection of production vs development environment
- Optional ENABLE_UPLOADS=true to force enable uploads in production
- Conditional route registration based on environment capabilities

This fixes the EACCES permission denied error when creating /app/uploads directory
in containerized environments like Hugging Face Spaces.

client/src/components/knowledge-base/document-upload.tsx CHANGED
@@ -148,11 +148,15 @@ export default function DocumentUpload() {
148
  body: formData,
149
  });
150
 
 
 
151
  if (!response.ok) {
152
- throw new Error(`Upload failed: ${response.statusText}`);
 
 
 
 
153
  }
154
-
155
- const result = await response.json();
156
 
157
  if (result.success && result.documents?.length > 0) {
158
  const document = result.documents[0];
 
148
  body: formData,
149
  });
150
 
151
+ const result = await response.json();
152
+
153
  if (!response.ok) {
154
+ // Handle disabled uploads gracefully
155
+ if (response.status === 503) {
156
+ throw new Error(result.message || 'File uploads are disabled in this environment');
157
+ }
158
+ throw new Error(result.message || `Upload failed: ${response.statusText}`);
159
  }
 
 
160
 
161
  if (result.success && result.documents?.length > 0) {
162
  const document = result.documents[0];
server/file-upload.ts CHANGED
@@ -4,24 +4,40 @@ import fs from 'fs';
4
  import crypto from 'crypto';
5
  import { Request } from 'express';
6
 
7
- // Ensure uploads directory exists
8
- const uploadsDir = path.join(process.cwd(), 'uploads');
9
- if (!fs.existsSync(uploadsDir)) {
10
- fs.mkdirSync(uploadsDir, { recursive: true });
 
 
 
 
 
 
 
 
 
 
11
  }
12
 
13
  // Configure multer for file uploads
14
  const storage = multer.diskStorage({
15
  destination: (req, file, cb) => {
16
- // Create subdirectories by date for organization
17
- const dateDir = new Date().toISOString().split('T')[0];
18
- const fullPath = path.join(uploadsDir, dateDir);
19
-
20
- if (!fs.existsSync(fullPath)) {
21
- fs.mkdirSync(fullPath, { recursive: true });
 
 
 
 
 
 
 
 
22
  }
23
-
24
- cb(null, fullPath);
25
  },
26
  filename: (req, file, cb) => {
27
  // Generate unique filename with timestamp and random string
 
4
  import crypto from 'crypto';
5
  import { Request } from 'express';
6
 
7
+ // Use /tmp for uploads in production environments (like Hugging Face Spaces)
8
+ // This ensures we have write permissions
9
+ const uploadsDir = process.env.NODE_ENV === 'production'
10
+ ? path.join('/tmp', 'uploads')
11
+ : path.join(process.cwd(), 'uploads');
12
+
13
+ // Safely create uploads directory with error handling
14
+ try {
15
+ if (!fs.existsSync(uploadsDir)) {
16
+ fs.mkdirSync(uploadsDir, { recursive: true });
17
+ }
18
+ } catch (error) {
19
+ console.warn(`Failed to create uploads directory at ${uploadsDir}:`, error);
20
+ console.log('File uploads will be disabled');
21
  }
22
 
23
  // Configure multer for file uploads
24
  const storage = multer.diskStorage({
25
  destination: (req, file, cb) => {
26
+ try {
27
+ // Create subdirectories by date for organization
28
+ const dateDir = new Date().toISOString().split('T')[0];
29
+ const fullPath = path.join(uploadsDir, dateDir);
30
+
31
+ if (!fs.existsSync(fullPath)) {
32
+ fs.mkdirSync(fullPath, { recursive: true });
33
+ }
34
+
35
+ cb(null, fullPath);
36
+ } catch (error) {
37
+ console.error('Failed to create upload directory:', error);
38
+ // Fallback to base uploads directory
39
+ cb(null, uploadsDir);
40
  }
 
 
41
  },
42
  filename: (req, file, cb) => {
43
  // Generate unique filename with timestamp and random string
server/routes.ts CHANGED
@@ -7,6 +7,7 @@ import { smartIngestionService } from "./smart-ingestion";
7
  import { nebiusClient } from "./nebius-client";
8
  import { modalClient } from "./modal-client";
9
  import documentRoutes from "./document-routes";
 
10
 
11
  interface GitHubRepo {
12
  id: number;
@@ -1140,8 +1141,16 @@ Provide a brief, engaging explanation (2-3 sentences) that would be pleasant to
1140
  }
1141
  });
1142
 
1143
- // Register document routes
1144
- app.use("/api/documents", documentRoutes);
 
 
 
 
 
 
 
 
1145
 
1146
  const httpServer = createServer(app);
1147
  return httpServer;
 
7
  import { nebiusClient } from "./nebius-client";
8
  import { modalClient } from "./modal-client";
9
  import documentRoutes from "./document-routes";
10
+ import uploadFallbackRoutes from "./upload-fallback";
11
 
12
  interface GitHubRepo {
13
  id: number;
 
1141
  }
1142
  });
1143
 
1144
+ // Register document routes - use fallback in production environments without persistent storage
1145
+ const isDocumentUploadEnabled = process.env.NODE_ENV !== 'production' || process.env.ENABLE_UPLOADS === 'true';
1146
+
1147
+ if (isDocumentUploadEnabled) {
1148
+ console.log('βœ… Document uploads enabled');
1149
+ app.use("/api/documents", documentRoutes);
1150
+ } else {
1151
+ console.log('ℹ️ Document uploads disabled - using fallback routes');
1152
+ app.use("/api/documents", uploadFallbackRoutes);
1153
+ }
1154
 
1155
  const httpServer = createServer(app);
1156
  return httpServer;
server/sqlite-storage.ts CHANGED
@@ -19,14 +19,25 @@ import { IStorage } from './storage';
19
  export class SQLiteStorage implements IStorage {
20
  private db: Database.Database;
21
 
22
- constructor(dbPath = './data/knowledgebridge.db') {
23
- // Ensure data directory exists
24
- const dir = path.dirname(dbPath);
25
- if (!fs.existsSync(dir)) {
26
- fs.mkdirSync(dir, { recursive: true });
 
 
 
 
 
 
 
 
 
 
 
27
  }
28
 
29
- this.db = new Database(dbPath);
30
  this.initializeTables();
31
  }
32
 
 
19
  export class SQLiteStorage implements IStorage {
20
  private db: Database.Database;
21
 
22
+ constructor(dbPath?: string) {
23
+ // Use /tmp for database in production environments (like Hugging Face Spaces)
24
+ const defaultPath = process.env.NODE_ENV === 'production'
25
+ ? '/tmp/knowledgebridge.db'
26
+ : './data/knowledgebridge.db';
27
+
28
+ const finalPath = dbPath || defaultPath;
29
+
30
+ // Ensure data directory exists with error handling
31
+ const dir = path.dirname(finalPath);
32
+ try {
33
+ if (!fs.existsSync(dir)) {
34
+ fs.mkdirSync(dir, { recursive: true });
35
+ }
36
+ } catch (error) {
37
+ console.warn(`Failed to create database directory at ${dir}:`, error);
38
  }
39
 
40
+ this.db = new Database(finalPath);
41
  this.initializeTables();
42
  }
43
 
server/upload-fallback.ts ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from 'express';
2
+
3
+ const router = Router();
4
+
5
+ /**
6
+ * Fallback routes for file upload functionality when uploads are disabled
7
+ * (e.g., in production environments without persistent storage)
8
+ */
9
+
10
+ router.post('/upload', async (req, res) => {
11
+ res.status(503).json({
12
+ success: false,
13
+ error: 'File uploads disabled',
14
+ message: 'File uploads are currently disabled in this environment. This is typically the case in demo environments or spaces without persistent storage.',
15
+ suggestion: 'You can still use the search functionality with external sources, or deploy the application with persistent storage to enable document uploads.'
16
+ });
17
+ });
18
+
19
+ router.post('/process/:id', async (req, res) => {
20
+ res.status(503).json({
21
+ success: false,
22
+ error: 'Document processing disabled',
23
+ message: 'Document processing is disabled when file uploads are not available.'
24
+ });
25
+ });
26
+
27
+ router.post('/process/batch', async (req, res) => {
28
+ res.status(503).json({
29
+ success: false,
30
+ error: 'Batch processing disabled',
31
+ message: 'Batch processing is disabled when file uploads are not available.'
32
+ });
33
+ });
34
+
35
+ router.post('/index/build', async (req, res) => {
36
+ res.status(503).json({
37
+ success: false,
38
+ error: 'Index building disabled',
39
+ message: 'Vector index building is disabled when document uploads are not available.'
40
+ });
41
+ });
42
+
43
+ router.post('/search/vector', async (req, res) => {
44
+ res.status(503).json({
45
+ success: false,
46
+ error: 'Vector search disabled',
47
+ message: 'Vector search is disabled when no documents have been uploaded and processed.'
48
+ });
49
+ });
50
+
51
+ router.get('/list', async (req, res) => {
52
+ res.json({
53
+ success: true,
54
+ documents: [],
55
+ totalCount: 0,
56
+ message: 'No documents available - file uploads are disabled in this environment'
57
+ });
58
+ });
59
+
60
+ router.get('/status/:id', async (req, res) => {
61
+ res.status(404).json({
62
+ success: false,
63
+ error: 'Document not found',
64
+ message: 'Document processing is disabled in this environment'
65
+ });
66
+ });
67
+
68
+ router.delete('/:id', async (req, res) => {
69
+ res.status(503).json({
70
+ success: false,
71
+ error: 'Document deletion disabled',
72
+ message: 'Document operations are disabled when file uploads are not available.'
73
+ });
74
+ });
75
+
76
+ export default router;