package server import ( "net/http" "os" "path/filepath" "strings" "time" ) // handleStatic serves static files from Odoo addon directories. // URL pattern: //static/ // Maps to: //static/ func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet && r.Method != http.MethodHead { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Serve the compiled XML templates bundle from memory (generated at // startup by compileXMLTemplates) instead of reading the pre-compiled // file from the build directory. This replaces the Python build step. if r.URL.Path == "/web/static/src/xml_templates_bundle.js" { w.Header().Set("Content-Type", "application/javascript; charset=utf-8") w.Header().Set("Cache-Control", "public, max-age=3600") w.Write([]byte(s.xmlTemplateBundle)) return } path := strings.TrimPrefix(r.URL.Path, "/") // Handle /odoo//static/ → treat as //static/ if strings.HasPrefix(path, "odoo/") { path = strings.TrimPrefix(path, "odoo/") } parts := strings.SplitN(path, "/", 3) if len(parts) < 3 || parts[1] != "static" { http.NotFound(w, r) return } addonName := parts[0] filePath := parts[2] // Security: prevent directory traversal if strings.Contains(filePath, "..") { http.NotFound(w, r) return } // For CSS files: check build dir first (compiled SCSS -> CSS) if s.config.BuildDir != "" && strings.HasSuffix(filePath, ".css") { buildPath := filepath.Join(s.config.BuildDir, addonName, "static", filePath) if _, err := os.Stat(buildPath); err == nil { w.Header().Set("Cache-Control", "public, max-age=3600") http.ServeFile(w, r, buildPath) return } } // Search in frontend directory if s.config.FrontendDir != "" { fullPath := filepath.Join(s.config.FrontendDir, addonName, "static", filePath) if _, err := os.Stat(fullPath); err == nil { w.Header().Set("Cache-Control", "public, max-age=3600") // Serve SCSS as compiled CSS if available if strings.HasSuffix(fullPath, ".scss") && s.config.BuildDir != "" { buildCSS := filepath.Join(s.config.BuildDir, addonName, "static", strings.TrimSuffix(filePath, ".scss")+".css") if _, err := os.Stat(buildCSS); err == nil { fullPath = buildCSS } } // Transpile ES module JS files on-the-fly when served // individually (e.g. debug mode). The main bundle already // contains transpiled versions, but individual file // requests still need transpilation. if strings.HasSuffix(fullPath, ".js") { data, err := os.ReadFile(fullPath) if err != nil { http.NotFound(w, r) return } content := string(data) urlPath := "/" + addonName + "/static/" + filePath if IsOdooModule(urlPath, content) { content = TranspileJS(urlPath, content) } w.Header().Set("Content-Type", "application/javascript; charset=utf-8") http.ServeContent(w, r, filePath, time.Time{}, strings.NewReader(content)) return } http.ServeFile(w, r, fullPath) return } } // Fallback: build dir for pre-compiled vendor assets if s.config.BuildDir != "" { fullPath := filepath.Join(s.config.BuildDir, addonName, "static", filePath) if _, err := os.Stat(fullPath); err == nil { w.Header().Set("Cache-Control", "public, max-age=3600") http.ServeFile(w, r, fullPath) return } } http.NotFound(w, r) }