package main import ( "bytes" "encoding/json" "fmt" "io" "mime/multipart" "net/http" urlpkg "net/url" "os" "path/filepath" "regexp" "strings" "time" ) func PlecPage(w http.ResponseWriter, r *http.Request) { body := ` PLEC Upload Pick HTML files, give each an iteration name, then upload them to PLEC. Inputs Select Folder Select File(s) Clear or drop HTML files or folders here (no files selected) FilenameIteration Name Upload Open Copy Link ` w.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = w.Write([]byte(Page("/plec", "PLEC Upload", body))) } func PlecPick(w http.ResponseWriter, r *http.Request) { cfg := LoadConfig() picked := PickFiles( "Select HTML", []FileFilter{{Name: "HTML", Extensions: []string{"html", "htm"}}}, true, cfg.LastPickDir, ) if len(picked) == 0 { writeJSON(w, map[string]any{"files": []any{}}) return } cfg.LastPickDir = filepath.Dir(picked[0]) _ = SaveConfig(cfg) files := make([]map[string]string, 0, len(picked)) for _, p := range picked { files = append(files, map[string]string{"path": p, "name": filepath.Base(p)}) } writeJSON(w, map[string]any{"files": files}) } func PlecPickFolder(w http.ResponseWriter, r *http.Request) { cfg := LoadConfig() dir := PickFolder("Select folder", cfg.LastPickDir) if dir == "" { writeJSON(w, map[string]any{"files": []any{}}) return } cfg.LastPickDir = dir _ = SaveConfig(cfg) paths := collectHTMLFiles(dir) files := make([]map[string]string, 0, len(paths)) for _, p := range paths { files = append(files, map[string]string{"path": p, "name": filepath.Base(p)}) } writeJSON(w, map[string]any{"files": files}) } type plecRow struct { ID string `json:"id"` Path string `json:"path"` Iteration string `json:"iteration"` } func PlecUpload(w http.ResponseWriter, r *http.Request) { var req struct { Rows []plecRow `json:"rows"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } cfg := LoadConfig().Plec type rowErr struct { ID string `json:"id"` Message string `json:"message"` } var rowErrors []rowErr var valid []plecRow htmlRx := regexp.MustCompile(`(?i)\.html?$`) for _, row := range req.Rows { if row.Path == "" || !fileExists(row.Path) { rowErrors = append(rowErrors, rowErr{row.ID, "File missing"}) } else if strings.TrimSpace(row.Iteration) == "" { rowErrors = append(rowErrors, rowErr{row.ID, "Iteration name required"}) } else if !htmlRx.MatchString(row.Path) { rowErrors = append(rowErrors, rowErr{row.ID, "Must be .html"}) } else { valid = append(valid, row) } } if len(rowErrors) > 0 { writeJSON(w, map[string]any{"rowErrors": rowErrors}) return } if len(valid) == 0 { writeJSON(w, map[string]any{"error": "Nothing to upload."}) return } stamp := time.Now().Format("20060102150405") var buf bytes.Buffer mw := multipart.NewWriter(&buf) for i, row := range valid { idx := i + 1 data, err := os.ReadFile(row.Path) if err != nil { writeJSON(w, map[string]any{"error": "Read failed: " + err.Error()}) return } stampedName := appendTimestamp(filepath.Base(row.Path), stamp) fw, err := createFormFileWithType(mw, fmt.Sprintf("fileUpload_%d", idx), stampedName, "text/html") if err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } if _, err := fw.Write(data); err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } iter := urlpkg.QueryEscape(strings.ReplaceAll(row.Iteration, " ", "")) // Original uses encodeURI which keeps more chars than QueryEscape; emulate by un-escaping safe chars. iter = encodeURICompatible(strings.ReplaceAll(row.Iteration, " ", "")) if err := mw.WriteField(fmt.Sprintf("iterationNameUpload_%d", idx), iter); err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } } if err := mw.Close(); err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } httpReq, err := http.NewRequest("POST", cfg.UploadUrl, &buf) if err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } httpReq.Header.Set("Content-Type", mw.FormDataContentType()) httpReq.Header.Set("Origin", cfg.OriginUrl) httpReq.Header.Set("Referer", cfg.OriginUrl+"/") httpReq.Header.Set("User-Agent", "Mozilla/5.0 (HPL-Toolbox-Standalone)") httpReq.Header.Set("X-Requested-With", "XMLHttpRequest") httpReq.Header.Set("Accept", "*/*") resp, err := http.DefaultClient.Do(httpReq) if err != nil { writeJSON(w, map[string]any{"error": "Network error: " + err.Error()}) return } defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) if resp.StatusCode < 200 || resp.StatusCode >= 300 { writeJSON(w, map[string]any{"error": fmt.Sprintf("HTTP %d\n%s", resp.StatusCode, truncate(string(respBody), 800))}) return } var parsed map[string]any if err := json.Unmarshal(respBody, &parsed); err != nil { writeJSON(w, map[string]any{"error": "Server returned non-JSON:\n" + truncate(string(respBody), 800)}) return } var preview any if v, ok := parsed["preview"]; ok && v != nil { preview = v } else if v, ok := parsed["previewURL"]; ok { preview = v } writeJSON(w, map[string]any{"preview": preview, "raw": parsed, "rawText": string(respBody)}) } func ClipboardEndpoint(w http.ResponseWriter, r *http.Request) { var req struct { Text string `json:"text"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, map[string]any{"ok": false, "error": err.Error()}) return } if err := CopyToClipboard(req.Text); err != nil { writeJSON(w, map[string]any{"ok": false, "error": err.Error()}) return } writeJSON(w, map[string]any{"ok": true}) } func OpenEndpoint(w http.ResponseWriter, r *http.Request) { var req struct { URL string `json:"url"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, map[string]any{"ok": false, "error": err.Error()}) return } OpenInBrowser(req.URL) writeJSON(w, map[string]any{"ok": true}) } func fileExists(p string) bool { _, err := os.Stat(p) return err == nil } func appendTimestamp(filename, stamp string) string { ext := filepath.Ext(filename) base := filename if ext != "" { base = filename[:len(filename)-len(ext)] } return base + "_" + stamp + ext } func truncate(s string, n int) string { if len(s) <= n { return s } return s[:n] } // createFormFileWithType is like multipart.Writer.CreateFormFile but lets us // override the Content-Type (CreateFormFile hardcodes "application/octet-stream"). func createFormFileWithType(mw *multipart.Writer, field, filename, contentType string) (io.Writer, error) { h := make(map[string][]string) h["Content-Disposition"] = []string{ fmt.Sprintf(`form-data; name=%q; filename=%q`, field, filename), } h["Content-Type"] = []string{contentType} return mw.CreatePart(h) } // encodeURICompatible mimics JS encodeURI() — keeps A-Za-z0-9 and these // reserved/mark chars: ;,/?:@&=+$-_.!~*'()# func encodeURICompatible(s string) string { keep := func(c byte) bool { if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') { return true } switch c { case ';', ',', '/', '?', ':', '@', '&', '=', '+', '$', '-', '_', '.', '!', '~', '*', '\'', '(', ')', '#': return true } return false } var b strings.Builder for i := 0; i < len(s); i++ { c := s[i] if keep(c) { b.WriteByte(c) } else { b.WriteString(fmt.Sprintf("%%%02X", c)) } } return b.String() }
Pick HTML files, give each an iteration name, then upload them to PLEC.