Switch main report page to better example
This is still pulling from our generic district mock, but it's still better than what we had. This also includes adding static content hosting for the bootstrap content on the public domain.
This commit is contained in:
parent
0f82e4c0ef
commit
9774452821
19 changed files with 276 additions and 313 deletions
399
sync/endpoint.go
Normal file
399
sync/endpoint.go
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
package sync
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Gleipnir-Technology/nidus-sync/api"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/auth"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/background"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/config"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/htmlpage"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/htmlpage/sync"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/skip2/go-qrcode"
|
||||
)
|
||||
|
||||
func Router() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
// Root is a special endpoint that is neither authenticated nor unauthenticated
|
||||
r.Get("/", getRoot)
|
||||
|
||||
// Unauthenticated endpoints
|
||||
r.Get("/arcgis/oauth/begin", getArcgisOauthBegin)
|
||||
r.Get("/arcgis/oauth/callback", getArcgisOauthCallback)
|
||||
r.Get("/favicon.ico", getFavicon)
|
||||
|
||||
r.Get("/mock", renderMock("mock-root"))
|
||||
r.Get("/mock/admin", renderMock("admin"))
|
||||
r.Get("/mock/admin/service-request", renderMock("admin-service-request"))
|
||||
r.Get("/mock/data-entry", renderMock("data-entry"))
|
||||
r.Get("/mock/data-entry/bad", renderMock("data-entry-bad"))
|
||||
r.Get("/mock/data-entry/good", renderMock("data-entry-good"))
|
||||
r.Get("/mock/dispatch", renderMock("dispatch"))
|
||||
r.Get("/mock/dispatch-results", renderMock("dispatch-results"))
|
||||
r.Get("/mock/report", renderMock("report"))
|
||||
r.Get("/mock/report/{code}", renderMock("report-detail"))
|
||||
r.Get("/mock/report/{code}/confirm", renderMock("report-confirmation"))
|
||||
r.Get("/mock/report/{code}/contribute", renderMock("report-contribute"))
|
||||
r.Get("/mock/report/{code}/evidence", renderMock("report-evidence"))
|
||||
r.Get("/mock/report/{code}/schedule", renderMock("report-schedule"))
|
||||
r.Get("/mock/report/{code}/update", renderMock("report-update"))
|
||||
r.Get("/mock/service-request", renderMock("service-request"))
|
||||
r.Get("/mock/service-request/{code}", renderMock("service-request-detail"))
|
||||
r.Get("/mock/service-request-location", renderMock("service-request-location"))
|
||||
r.Get("/mock/service-request-mosquito", renderMock("service-request-mosquito"))
|
||||
r.Get("/mock/service-request-pool", renderMock("service-request-pool"))
|
||||
r.Get("/mock/service-request-quick", renderMock("service-request-quick"))
|
||||
r.Get("/mock/service-request-quick-confirmation", renderMock("service-request-quick-confirmation"))
|
||||
r.Get("/mock/service-request-updates", renderMock("service-request-updates"))
|
||||
r.Get("/mock/setting", renderMock("setting-mock"))
|
||||
r.Get("/mock/setting/integration", renderMock("setting-integration"))
|
||||
r.Get("/mock/setting/pesticide", renderMock("setting-pesticide"))
|
||||
r.Get("/mock/setting/pesticide/add", renderMock("setting-pesticide-add"))
|
||||
r.Get("/mock/setting/user", renderMock("setting-user"))
|
||||
r.Get("/mock/setting/user/add", renderMock("setting-user-add"))
|
||||
|
||||
r.Get("/oauth/refresh", getOAuthRefresh)
|
||||
|
||||
r.Get("/qr-code/report/{code}", getQRCodeReport)
|
||||
r.Get("/signin", getSignin)
|
||||
r.Post("/signin", postSignin)
|
||||
r.Get("/signup", getSignup)
|
||||
r.Post("/signup", postSignup)
|
||||
r.Get("/sms", getSMS)
|
||||
r.Post("/sms", postSMS)
|
||||
r.Get("/sms.php", getSMS)
|
||||
r.Get("/sms/{org}", getSMS)
|
||||
r.Post("/sms/{org}", postSMS)
|
||||
|
||||
// Authenticated endpoints
|
||||
r.Route("/api", api.AddRoutes)
|
||||
r.Method("GET", "/cell/{cell}", auth.NewEnsureAuth(getCellDetails))
|
||||
r.Method("GET", "/settings", auth.NewEnsureAuth(getSettings))
|
||||
r.Method("GET", "/source/{globalid}", auth.NewEnsureAuth(getSource))
|
||||
//r.Method("GET", "/vector-tiles/{org_id}/{tileset_id}/{zoom}/{x}/{y}.{format}", auth.NewEnsureAuth(getVectorTiles))
|
||||
|
||||
localFS := http.Dir("./static")
|
||||
htmlpage.FileServer(r, "/static", localFS, sync.EmbeddedStaticFS, "static")
|
||||
return r
|
||||
}
|
||||
|
||||
func getArcgisOauthBegin(w http.ResponseWriter, r *http.Request) {
|
||||
authURL := config.BuildArcGISAuthURL(config.ClientID)
|
||||
http.Redirect(w, r, authURL, http.StatusFound)
|
||||
}
|
||||
|
||||
func getArcgisOauthCallback(w http.ResponseWriter, r *http.Request) {
|
||||
code := r.URL.Query().Get("code")
|
||||
log.Info().Str("code", code).Msg("Handling oauth callback")
|
||||
if code == "" {
|
||||
respondError(w, "Access code is empty", nil, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
user, err := auth.GetAuthenticatedUser(r)
|
||||
if err != nil {
|
||||
respondError(w, "You're not currently authenticated, which really shouldn't happen.", err, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
err = background.HandleOauthAccessCode(r.Context(), user, code)
|
||||
if err != nil {
|
||||
respondError(w, "Failed to handle access code", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, config.MakeURLSync("/"), http.StatusFound)
|
||||
}
|
||||
|
||||
func getCellDetails(w http.ResponseWriter, r *http.Request, user *models.User) {
|
||||
cell_str := chi.URLParam(r, "cell")
|
||||
if cell_str == "" {
|
||||
respondError(w, "There should always be a cell", nil, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
cell, err := HexToInt64(cell_str)
|
||||
if err != nil {
|
||||
respondError(w, "Cannot convert provided cell to uint64", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sync.Cell(r.Context(), w, user, cell)
|
||||
}
|
||||
|
||||
func getFavicon(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-type", "image/x-icon")
|
||||
|
||||
http.ServeFile(w, r, "static/favicon.ico")
|
||||
}
|
||||
|
||||
func getOAuthRefresh(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := auth.GetAuthenticatedUser(r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/?next=/oauth/refresh", http.StatusFound)
|
||||
return
|
||||
}
|
||||
sync.OauthPrompt(w, user)
|
||||
}
|
||||
|
||||
func getQRCodeReport(w http.ResponseWriter, r *http.Request) {
|
||||
code := chi.URLParam(r, "code")
|
||||
if code == "" {
|
||||
respondError(w, "There should always be a code", nil, http.StatusBadRequest)
|
||||
}
|
||||
content := config.MakeURLSync("/report/" + code)
|
||||
// Get optional size parameter (default to 256)
|
||||
size := 256
|
||||
if sizeStr := r.URL.Query().Get("size"); sizeStr != "" {
|
||||
var err error
|
||||
size, err = strconv.Atoi(sizeStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid 'size' parameter, must be an integer", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get optional error correction level (default to Medium)
|
||||
level := qrcode.Medium
|
||||
if levelStr := r.URL.Query().Get("level"); levelStr != "" {
|
||||
switch levelStr {
|
||||
case "L", "l":
|
||||
level = qrcode.Low
|
||||
case "M", "m":
|
||||
level = qrcode.Medium
|
||||
case "Q", "q":
|
||||
level = qrcode.High
|
||||
case "H", "h":
|
||||
level = qrcode.Highest
|
||||
default:
|
||||
respondError(w, "Invalid 'level' parameter, must be L, M, Q, or H", nil, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the QR code
|
||||
var qr *qrcode.QRCode
|
||||
var err error
|
||||
qr, err = qrcode.New(content, level)
|
||||
if err != nil {
|
||||
respondError(w, "Error generating QR code", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Set the appropriate content type
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
|
||||
// Generate PNG and write directly to the response writer
|
||||
png, err := qr.PNG(size)
|
||||
if err != nil {
|
||||
respondError(w, "Error encoding QR code to PNG", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = w.Write(png)
|
||||
if err != nil {
|
||||
respondError(w, "Error writing response", err, http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func getRoot(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := auth.GetAuthenticatedUser(r)
|
||||
if err != nil {
|
||||
// No credentials or user not found: go to login
|
||||
if errors.Is(err, &auth.NoCredentialsError{}) || errors.Is(err, &auth.NoUserError{}) {
|
||||
http.Redirect(w, r, "/signin", http.StatusFound)
|
||||
return
|
||||
} else {
|
||||
respondError(w, "Failed to get root", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
if user == nil {
|
||||
errorCode := r.URL.Query().Get("error")
|
||||
sync.Signin(w, errorCode)
|
||||
return
|
||||
} else {
|
||||
has, err := background.HasFieldseekerConnection(r.Context(), user)
|
||||
if err != nil {
|
||||
respondError(w, "Failed to check for ArcGIS connection", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if has {
|
||||
sync.Dashboard(r.Context(), w, user)
|
||||
return
|
||||
} else {
|
||||
sync.OauthPrompt(w, user)
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
respondError(w, "Failed to render root", err, http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func getSettings(w http.ResponseWriter, r *http.Request, u *models.User) {
|
||||
sync.Settings(w, r, u)
|
||||
}
|
||||
|
||||
func getSignin(w http.ResponseWriter, r *http.Request) {
|
||||
errorCode := r.URL.Query().Get("error")
|
||||
sync.Signin(w, errorCode)
|
||||
}
|
||||
|
||||
func getSignup(w http.ResponseWriter, r *http.Request) {
|
||||
sync.Signup(w, r.URL.Path)
|
||||
}
|
||||
|
||||
func getSource(w http.ResponseWriter, r *http.Request, u *models.User) {
|
||||
globalid_s := chi.URLParam(r, "globalid")
|
||||
if globalid_s == "" {
|
||||
respondError(w, "No globalid provided", nil, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
globalid, err := uuid.Parse(globalid_s)
|
||||
if err != nil {
|
||||
respondError(w, "globalid is not a UUID", nil, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sync.Source(w, r, u, globalid)
|
||||
}
|
||||
|
||||
func postSMS(w http.ResponseWriter, r *http.Request) {
|
||||
// Log all request headers
|
||||
for name, values := range r.Header {
|
||||
for _, value := range values {
|
||||
log.Info().Str("name", name).Str("value", value).Msg("header")
|
||||
}
|
||||
}
|
||||
|
||||
// Read the request body
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
//return nil, fmt.Errorf("failed to read request body: %w", err)
|
||||
respondError(w, "Failed to read request body", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.Info().Str("body", string(bodyBytes)).Msg("body")
|
||||
// Close the original body
|
||||
defer r.Body.Close()
|
||||
|
||||
// Parse JSON into webhook struct
|
||||
var body SMSWebhookBody
|
||||
if err := json.Unmarshal(bodyBytes, &body); err != nil {
|
||||
respondError(w, "Failed to parse JSON", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := handleSMSMessage(&body.Data); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to handle SMS Message")
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ok"))
|
||||
}
|
||||
func getSMS(w http.ResponseWriter, r *http.Request) {
|
||||
org := chi.URLParam(r, "org")
|
||||
|
||||
to := r.URL.Query().Get("error")
|
||||
from := r.URL.Query().Get("error")
|
||||
message := r.URL.Query().Get("error")
|
||||
files := r.URL.Query().Get("error")
|
||||
id := r.URL.Query().Get("error")
|
||||
date := r.URL.Query().Get("error")
|
||||
|
||||
log.Info().Str("org", org).Str("to", to).Str("from", from).Str("message", message).Str("files", files).Str("id", id).Str("date", date).Msg("Got SMS Message")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-type", "text/plain")
|
||||
// Signifies to Voip.ms that the callback worked.
|
||||
fmt.Fprintf(w, "ok")
|
||||
}
|
||||
func getVectorTiles(w http.ResponseWriter, r *http.Request, u *models.User) {
|
||||
org_id := chi.URLParam(r, "org_id")
|
||||
tileset_id := chi.URLParam(r, "tileset_id")
|
||||
zoom := chi.URLParam(r, "zoom")
|
||||
x := chi.URLParam(r, "x")
|
||||
y := chi.URLParam(r, "y")
|
||||
format := chi.URLParam(r, "format")
|
||||
|
||||
log.Info().Str("org_id", org_id).Str("tileset_id", tileset_id).Str("zoom", zoom).Str("x", x).Str("y", y).Str("format", format).Msg("Get vector tiles")
|
||||
|
||||
}
|
||||
|
||||
// Respond with an error that is visible to the user
|
||||
func respondError(w http.ResponseWriter, m string, e error, s int) {
|
||||
log.Warn().Int("status", s).Err(e).Str("user message", m).Msg("Responding with an error")
|
||||
http.Error(w, m, s)
|
||||
}
|
||||
|
||||
func postSignin(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
respondError(w, "Could not parse form", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
|
||||
log.Info().Str("username", username).Msg("Signin")
|
||||
|
||||
_, err := auth.SigninUser(r, username, password)
|
||||
if err != nil {
|
||||
if errors.Is(err, auth.InvalidCredentials{}) {
|
||||
http.Redirect(w, r, "/signin?error=invalid-credentials", http.StatusFound)
|
||||
return
|
||||
}
|
||||
if errors.Is(err, auth.InvalidUsername{}) {
|
||||
http.Redirect(w, r, "/signin?error=invalid-credentials", http.StatusFound)
|
||||
return
|
||||
}
|
||||
respondError(w, "Failed to signin user", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
|
||||
func postSignup(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
respondError(w, "Could not parse form", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
username := r.FormValue("username")
|
||||
name := r.FormValue("name")
|
||||
password := r.FormValue("password")
|
||||
terms := r.FormValue("terms")
|
||||
|
||||
log.Info().Str("username", username).Str("name", name).Str("password", strings.Repeat("*", len(password))).Msg("Signup")
|
||||
|
||||
if terms != "on" {
|
||||
log.Warn().Msg("Terms not agreed")
|
||||
http.Error(w, "You must agree to the terms to register", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := auth.SignupUser(r.Context(), username, name, password)
|
||||
if err != nil {
|
||||
respondError(w, "Failed to signup user", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
auth.AddUserSession(r, user)
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
}
|
||||
|
||||
func renderMock(templateName string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
code := chi.URLParam(r, "code")
|
||||
if code == "" {
|
||||
code = "abc-123"
|
||||
}
|
||||
sync.Mock(templateName, w, code)
|
||||
}
|
||||
}
|
||||
150
sync/sms.go
Normal file
150
sync/sms.go
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
package sync
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type SMSWebhookBody struct {
|
||||
Data SMSWebhookData `json:"data"`
|
||||
}
|
||||
type SMSWebhookData struct {
|
||||
ID int64 `json:"id"`
|
||||
EventType string `json:"event_type"`
|
||||
RecordType string `json:"record_type"`
|
||||
Payload SMSMessagePayload `json:"payload"`
|
||||
}
|
||||
|
||||
type SMSMessagePayload struct {
|
||||
ID int64 `json:"id"`
|
||||
RecordType string `json:"record_type"`
|
||||
From SMSContact `json:"from"`
|
||||
To []SMSContact `json:"to"`
|
||||
Text string `json:"text"`
|
||||
ReceivedAt string `json:"received_at"`
|
||||
Type string `json:"type"`
|
||||
Media []MMSMedia `json:"media"`
|
||||
}
|
||||
|
||||
type MMSMedia struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// Contact represents a phone contact
|
||||
type SMSContact struct {
|
||||
PhoneNumber string `json:"phone_number"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
func handleSMSMessage(data *SMSWebhookData) error {
|
||||
log.Info().Int64("ID", data.ID).Str("event_type", data.EventType).Str("record_type", data.RecordType).Str("from", data.Payload.From.PhoneNumber).Str("msg", data.Payload.Text).Str("receieved", data.Payload.ReceivedAt).Msg("Got SMS Message")
|
||||
|
||||
for _, media := range data.Payload.Media {
|
||||
filePath, err := downloadMedia(media.URL)
|
||||
if err != nil {
|
||||
fmt.Errorf("Failed to download media from %s: %w", filePath, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("Downloaded media to: %s\n", filePath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DownloadMedia downloads a media file from the given URL to a temporary location
|
||||
// and returns the path to the downloaded file
|
||||
func downloadMedia(mediaURL string) (string, error) {
|
||||
// Make GET request to the media URL
|
||||
resp, err := http.Get(mediaURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to download media: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("failed to download media: status code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Extract filename from URL or headers
|
||||
filename := getFilenameFromURL(mediaURL, resp)
|
||||
|
||||
// Create temporary file with proper extension
|
||||
tmpDir := os.TempDir()
|
||||
timestamp := time.Now().UnixNano()
|
||||
tmpFilePath := filepath.Join(tmpDir, fmt.Sprintf("media_%d_%s", timestamp, filename))
|
||||
|
||||
// Create the file
|
||||
out, err := os.Create(tmpFilePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temporary file: %w", err)
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Write the response body to the file
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to save media file: %w", err)
|
||||
}
|
||||
|
||||
return tmpFilePath, nil
|
||||
}
|
||||
|
||||
// getFilenameFromURL extracts filename from URL or Content-Disposition header
|
||||
func getFilenameFromURL(mediaURL string, resp *http.Response) string {
|
||||
// First try Content-Disposition header
|
||||
contentDisp := resp.Header.Get("Content-Disposition")
|
||||
if contentDisp != "" {
|
||||
if strings.Contains(contentDisp, "filename=") {
|
||||
parts := strings.Split(contentDisp, "filename=")
|
||||
if len(parts) > 1 {
|
||||
filename := strings.Trim(parts[1], "\"' ")
|
||||
if filename != "" {
|
||||
return sanitizeFilename(filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to URL path
|
||||
urlPath := path.Base(mediaURL)
|
||||
if urlPath != "" && urlPath != "." && urlPath != "/" {
|
||||
return sanitizeFilename(urlPath)
|
||||
}
|
||||
|
||||
// Default to generic name with extension based on Content-Type
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
ext := ".bin"
|
||||
|
||||
switch {
|
||||
case strings.Contains(contentType, "image/jpeg"):
|
||||
ext = ".jpg"
|
||||
case strings.Contains(contentType, "image/png"):
|
||||
ext = ".png"
|
||||
case strings.Contains(contentType, "image/gif"):
|
||||
ext = ".gif"
|
||||
case strings.Contains(contentType, "video/mp4"):
|
||||
ext = ".mp4"
|
||||
case strings.Contains(contentType, "audio/mpeg"):
|
||||
ext = ".mp3"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("media%s", ext)
|
||||
}
|
||||
|
||||
// sanitizeFilename removes potentially unsafe characters from filename
|
||||
func sanitizeFilename(name string) string {
|
||||
// Replace unsafe characters with underscore
|
||||
unsafe := []string{"/", "\\", "?", "%", "*", ":", "|", "\"", "<", ">"}
|
||||
result := name
|
||||
for _, c := range unsafe {
|
||||
result = strings.ReplaceAll(result, c, "_")
|
||||
}
|
||||
return result
|
||||
}
|
||||
34
sync/strings.go
Normal file
34
sync/strings.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package sync
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func HexToInt64(hexStr string) (int64, error) {
|
||||
// Remove "0x" prefix if present
|
||||
hexStr = strings.TrimPrefix(hexStr, "0x")
|
||||
hexStr = strings.TrimPrefix(hexStr, "0X")
|
||||
|
||||
// Parse hex string to uint64
|
||||
value, err := strconv.ParseInt(hexStr, 16, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func HexToUint64(hexStr string) (uint64, error) {
|
||||
// Remove "0x" prefix if present
|
||||
hexStr = strings.TrimPrefix(hexStr, "0x")
|
||||
hexStr = strings.TrimPrefix(hexStr, "0X")
|
||||
|
||||
// Parse hex string to uint64
|
||||
value, err := strconv.ParseUint(hexStr, 16, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue