-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.go
More file actions
287 lines (244 loc) · 7.63 KB
/
main.go
File metadata and controls
287 lines (244 loc) · 7.63 KB
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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
package main
import (
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
log "github.com/sirupsen/logrus"
"github.com/gin-gonic/gin"
)
// Config represents the application configuration
type Config struct {
Port string `json:"port"`
UploadDirectory string `json:"uploadDirectory"`
FileTypeOpeners map[string]string `json:"fileTypeOpeners"`
DefaultOpener string `json:"defaultOpener"`
MaxUploadSize int64 `json:"maxUploadSize"`
AllowedExtensions []string `json:"allowedExtensions"`
}
// Global configuration
var config Config
func init() {
// Set default configuration
config = Config{
Port: "8080",
UploadDirectory: "/tmp/agent-uploads",
FileTypeOpeners: map[string]string{
".pdf": "okular",
".txt": "gedit",
".png": "eog",
".jpg": "eog",
".jpeg": "eog",
".webp": "eog",
".gif": "eog",
".mp4": "vlc",
".mp3": "vlc",
".docx": "desktopeditors",
".xlsx": "desktopeditors",
".pptx": "desktopeditors",
".odt": "desktopeditors",
".ods": "desktopeditors",
".odp": "desktopeditors",
".csv": "desktopeditors",
// Add archive file formats with xarchiver
".zip": "file-roller",
".tar": "file-roller",
".gz": "file-roller",
".bz2": "file-roller",
".xz": "file-roller",
".7z": "file-roller",
".rar": "file-roller",
// Additional video formats for vlc
".avi": "vlc",
".mkv": "vlc",
".mov": "vlc",
".wmv": "vlc",
".flv": "vlc",
".webm": "vlc",
},
DefaultOpener: "xdg-open",
MaxUploadSize: 50 * 1024 * 1024, // 50MB
AllowedExtensions: []string{".pdf", ".txt", ".png", ".jpg", ".jpeg", ".gif", ".mp4", ".mp3", ".docx", ".xlsx", ".pptx", ".odt", ".ods", ".odp", ".csv",
".zip", ".tar", ".gz", ".bz2", ".xz", ".7z", ".rar", ".avi", ".mkv", ".mov", ".wmv", ".flv", ".webm"},
}
// Create upload directory if it doesn't exist
if err := os.MkdirAll(config.UploadDirectory, 0755); err != nil {
log.Fatalf("Failed to create upload directory: %v", err)
}
}
// FileResponse represents the response sent after file upload
type FileResponse struct {
Success bool `json:"success"`
FilePath string `json:"filePath,omitempty"`
ErrorMessage string `json:"errorMessage,omitempty"`
}
func main() {
// Create a default gin router with Logger and Recovery middleware
r := gin.Default()
// Add CORS middleware
r.Use(corsMiddleware())
// Routes
r.GET("/health", healthCheckHandler)
r.POST("/upload", uploadFileHandler)
r.GET("/open/:filename", openFileHandler)
// Start the server
log.Printf("File Opener Agent starting on port %s...", config.Port)
log.Printf("Upload directory: %s", config.UploadDirectory)
log.Fatal(r.Run(":" + config.Port))
}
// corsMiddleware adds CORS headers to responses
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusOK)
return
}
c.Next()
}
}
// healthCheckHandler returns a simple health check response
func healthCheckHandler(c *gin.Context) {
response := map[string]string{
"status": "OK",
"timestamp": time.Now().Format(time.RFC3339),
}
c.JSON(http.StatusOK, response)
}
// uploadFileHandler handles file uploads
func uploadFileHandler(c *gin.Context) {
// Limit request size
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, config.MaxUploadSize)
// Get file from request
file, header, err := c.Request.FormFile("file")
if err != nil {
respondWithError(c, "Failed to get file from request: "+err.Error(), http.StatusBadRequest)
return
}
defer file.Close()
// Validate file extension
ext := strings.ToLower(filepath.Ext(header.Filename))
if !contains(config.AllowedExtensions, ext) && len(config.AllowedExtensions) > 0 {
respondWithError(c, "File type not allowed", http.StatusBadRequest)
return
}
// Generate unique filename to prevent collisions
filename := fmt.Sprintf("%d_%s", time.Now().UnixNano(), header.Filename)
filePath := filepath.Join(config.UploadDirectory, filename)
// Create destination file
dst, err := os.Create(filePath)
if err != nil {
respondWithError(c, "Failed to create destination file: "+err.Error(), http.StatusInternalServerError)
return
}
defer dst.Close()
// Copy uploaded file to destination
if _, err := io.Copy(dst, file); err != nil {
respondWithError(c, "Failed to save file: "+err.Error(), http.StatusInternalServerError)
return
}
// Check if we should automatically open the file
openNow := c.PostForm("openNow")
if openNow == "true" {
go openFile(filePath)
}
// Return success response
c.JSON(http.StatusOK, FileResponse{
Success: true,
FilePath: filePath,
})
}
// openFileHandler opens a previously uploaded file
func openFileHandler(c *gin.Context) {
filename := c.Param("filename")
// Ensure filename is safe (no directory traversal)
if strings.Contains(filename, "..") {
respondWithError(c, "Invalid filename", http.StatusBadRequest)
return
}
filePath := filepath.Join(config.UploadDirectory, filename)
// Check if file exists
if _, err := os.Stat(filePath); os.IsNotExist(err) {
respondWithError(c, "File not found", http.StatusNotFound)
return
}
// Create a channel to signal when the file open operation is complete
done := make(chan bool, 1)
// Run the file handling in a goroutine
go func() {
openFile(filePath)
done <- true
}()
// Respond immediately without waiting for the open operation to complete
c.JSON(http.StatusOK, FileResponse{
Success: true,
FilePath: filePath,
})
// Optionally, wait for the operation to complete in the background
// This is non-blocking for the HTTP response
go func() {
<-done
log.Printf("File opening operation completed for %s", filePath)
}()
}
// openFile opens a file with the appropriate application based on file type
func openFile(filePath string) {
ext := strings.ToLower(filepath.Ext(filePath))
opener, exists := config.FileTypeOpeners[ext]
if !exists {
opener = config.DefaultOpener
}
// Check if the application exists/is available
_, err := exec.LookPath(opener)
if err != nil {
log.Printf("Warning: %s not found in PATH, falling back to default opener", opener)
opener = config.DefaultOpener
}
// Add command-line options for specific applications
var cmd *exec.Cmd
switch opener {
case "xarchiver":
// xarchiver needs special handling to properly open archives
cmd = exec.Command(opener, "--extract-to="+filepath.Dir(filePath), filePath)
case "vlc":
// vlc might need some specific options
cmd = exec.Command(opener, "--no-video-title-show", filePath)
default:
cmd = exec.Command(opener, filePath)
}
// Start the application
err = cmd.Start()
if err != nil {
log.Printf("Error opening %s with %s: %v", filePath, opener, err)
// Try fallback to default opener
if opener != config.DefaultOpener {
fallbackCmd := exec.Command(config.DefaultOpener, filePath)
fallbackCmd.Start()
log.Printf("Attempted fallback to %s", config.DefaultOpener)
}
} else {
log.Printf("Opened %s with %s", filePath, opener)
}
}
// respondWithError sends an error response
func respondWithError(c *gin.Context, message string, statusCode int) {
c.JSON(statusCode, FileResponse{
Success: false,
ErrorMessage: message,
})
}
// contains checks if a string slice contains a specific string
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}