Skip to content

Commit 9a8a681

Browse files
AchoArnoldCopilot
andauthored
refactor: use timestamp+filename for bulk message request ID (#903)
* refactor: use timestamp+filename for bulk message request ID - Generate request ID as bulk-{base62_timestamp}-{truncated_filename} - Encode unix timestamp as base62 for minimal length (~6 chars) - Truncate filename to max 32 chars preserving extension - Remove fileType return value from ValidateStore - Simplify frontend cleanName() to just strip 'bulk-' prefix - Stay on bulk-messages page after upload instead of redirecting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address PR review comments - Add 4-char random base62 suffix to prevent same-second collisions - Sanitize filename by stripping non-alphanumeric chars (except . and -) - Restore backward-compat in cleanName for old bulk-csv-/bulk-xls- entries Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: allow space character in sanitizeFilename Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 808c0e1 commit 9a8a681

5 files changed

Lines changed: 53 additions & 38 deletions

File tree

api/go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ require (
3333
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
3434
github.com/jszwec/csvutil v1.10.0
3535
github.com/lib/pq v1.12.3
36-
github.com/matoous/go-nanoid/v2 v2.1.0
3736
github.com/nyaruka/phonenumbers v1.7.2
3837
github.com/palantir/stacktrace v0.0.0-20161112013806-78658fd2d177
3938
github.com/patrickmn/go-cache v2.1.0+incompatible

api/go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
237237
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
238238
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
239239
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
240-
github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE=
241-
github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM=
242240
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
243241
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
244242
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=

api/pkg/handlers/bulk_message_handler.go

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
package handlers
22

33
import (
4-
"crypto/rand"
54
"fmt"
5+
"path/filepath"
6+
"regexp"
67
"sync"
78
"sync/atomic"
9+
"time"
810

911
"github.com/NdoleStudio/httpsms/pkg/requests"
1012
"github.com/NdoleStudio/httpsms/pkg/services"
1113
"github.com/NdoleStudio/httpsms/pkg/telemetry"
1214
"github.com/NdoleStudio/httpsms/pkg/validators"
1315
"github.com/davecgh/go-spew/spew"
1416
"github.com/gofiber/fiber/v2"
15-
gonanoid "github.com/matoous/go-nanoid/v2"
1617
"github.com/palantir/stacktrace"
1718
)
1819

@@ -99,7 +100,7 @@ func (h *BulkMessageHandler) Store(c *fiber.Ctx) error {
99100
return h.responseBadRequest(c, err)
100101
}
101102

102-
messages, fileType, userLocation, validationErrors := h.validator.ValidateStore(ctx, h.userIDFomContext(c), file)
103+
messages, userLocation, validationErrors := h.validator.ValidateStore(ctx, h.userIDFomContext(c), file)
103104
if len(validationErrors) != 0 {
104105
msg := fmt.Sprintf("validation errors [%s], while sending bulk sms from CSV file [%s] for [%s]", spew.Sdump(validationErrors), file.Filename, h.userIDFomContext(c))
105106
ctxLogger.Warn(stacktrace.NewError(msg))
@@ -111,7 +112,7 @@ func (h *BulkMessageHandler) Store(c *fiber.Ctx) error {
111112
return h.responsePaymentRequired(c, *msg)
112113
}
113114

114-
requestID := h.generateRequestID(fileType, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
115+
requestID := h.generateRequestID(file.Filename)
115116
wg := sync.WaitGroup{}
116117
count := atomic.Int64{}
117118

@@ -145,21 +146,41 @@ func (h *BulkMessageHandler) Store(c *fiber.Ctx) error {
145146
return h.responseAccepted(c, fmt.Sprintf("Added %d out of %d messages to the queue", count.Load(), len(messages)))
146147
}
147148

148-
func (h *BulkMessageHandler) generateRequestID(fileType string, alphabet string) string {
149-
id, err := gonanoid.Generate(alphabet, 10)
150-
if err != nil {
151-
id = h.randomAlphaNum(10, alphabet)
149+
func (h *BulkMessageHandler) generateRequestID(filename string) string {
150+
return fmt.Sprintf("bulk-%s-%s", encodeBase62(time.Now().Unix()), truncateFilename(sanitizeFilename(filename), 32))
151+
}
152+
153+
func sanitizeFilename(filename string) string {
154+
return regexp.MustCompile(`[^a-zA-Z0-9.\-_: ]`).ReplaceAllString(filename, "")
155+
}
156+
157+
func encodeBase62(n int64) string {
158+
const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
159+
if n == 0 {
160+
return "0"
161+
}
162+
result := make([]byte, 0, 8)
163+
for n > 0 {
164+
result = append(result, charset[n%62])
165+
n /= 62
166+
}
167+
// reverse
168+
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
169+
result[i], result[j] = result[j], result[i]
152170
}
153-
return fmt.Sprintf("bulk-%s-%s", fileType, id)
171+
return string(result)
154172
}
155173

156-
func (h *BulkMessageHandler) randomAlphaNum(length int, alphabet string) string {
157-
b := make([]byte, length)
158-
if _, err := rand.Read(b); err != nil {
159-
return alphabet[:length]
174+
func truncateFilename(filename string, maxLen int) string {
175+
if len(filename) <= maxLen {
176+
return filename
160177
}
161-
for i := range b {
162-
b[i] = alphabet[int(b[i])%len(alphabet)]
178+
ext := filepath.Ext(filename)
179+
name := filename[:len(filename)-len(ext)]
180+
available := maxLen - len(ext)
181+
if available <= 0 {
182+
return filename[:maxLen]
163183
}
164-
return string(b)
184+
half := available / 2
185+
return name[:half] + name[len(name)-(available-half):] + ext
165186
}

api/pkg/validators/bulk_message_handler_validator.go

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func NewBulkMessageHandlerValidator(
5252
}
5353

5454
// ValidateStore validates the requests.BillingUsageHistory request
55-
func (v *BulkMessageHandlerValidator) ValidateStore(ctx context.Context, userID entities.UserID, header *multipart.FileHeader) ([]*requests.BulkMessage, string, *time.Location, url.Values) {
55+
func (v *BulkMessageHandlerValidator) ValidateStore(ctx context.Context, userID entities.UserID, header *multipart.FileHeader) ([]*requests.BulkMessage, *time.Location, url.Values) {
5656
ctx, span, ctxLogger := v.tracer.StartWithLogger(ctx, v.logger)
5757
defer span.End()
5858

@@ -61,22 +61,22 @@ func (v *BulkMessageHandlerValidator) ValidateStore(ctx context.Context, userID
6161
result := url.Values{}
6262
result.Add("document", "Cannot load your account. Please try again later or contact support.")
6363
ctxLogger.Error(v.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, fmt.Sprintf("cannot load user [%s]", userID))))
64-
return nil, "", nil, result
64+
return nil, nil, result
6565
}
6666

67-
messages, fileType, result := v.parseFile(ctxLogger, user, header)
67+
messages, result := v.parseFile(ctxLogger, user, header)
6868
if len(result) != 0 {
69-
return messages, fileType, user.Location(), result
69+
return messages, user.Location(), result
7070
}
7171

7272
if len(messages) == 0 {
7373
result.Add("document", "The uploaded file doesn't contain any valid records. Make sure you are using the official httpSMS template.")
74-
return messages, fileType, user.Location(), result
74+
return messages, user.Location(), result
7575
}
7676

7777
if len(messages) > 1000 {
7878
result.Add("document", "The uploaded file must contain less than 1000 records.")
79-
return messages, fileType, user.Location(), result
79+
return messages, user.Location(), result
8080
}
8181

8282
for index, message := range messages {
@@ -85,32 +85,30 @@ func (v *BulkMessageHandlerValidator) ValidateStore(ctx context.Context, userID
8585

8686
result = v.validateMessages(ctx, messages, user.Location())
8787
if len(result) != 0 {
88-
return messages, fileType, user.Location(), result
88+
return messages, user.Location(), result
8989
}
9090

9191
result = v.validateOwners(ctx, userID, messages)
9292
if len(result) != 0 {
93-
return messages, fileType, user.Location(), result
93+
return messages, user.Location(), result
9494
}
9595

96-
return messages, fileType, user.Location(), result
96+
return messages, user.Location(), result
9797
}
9898

99-
func (v *BulkMessageHandlerValidator) parseFile(ctxLogger telemetry.Logger, user *entities.User, header *multipart.FileHeader) ([]*requests.BulkMessage, string, url.Values) {
99+
func (v *BulkMessageHandlerValidator) parseFile(ctxLogger telemetry.Logger, user *entities.User, header *multipart.FileHeader) ([]*requests.BulkMessage, url.Values) {
100100
if header.Header.Get("Content-Type") == "text/csv" || strings.HasSuffix(header.Filename, ".csv") {
101-
messages, result := v.parseCSV(ctxLogger, user, header)
102-
return messages, "csv", result
101+
return v.parseCSV(ctxLogger, user, header)
103102
}
104103
if header.Header.Get("Content-Type") == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" || strings.HasSuffix(header.Filename, ".xlsx") {
105-
messages, result := v.parseXlsx(ctxLogger, user, header)
106-
return messages, "xls", result
104+
return v.parseXlsx(ctxLogger, user, header)
107105
}
108106

109107
ctxLogger.Error(stacktrace.NewError(fmt.Sprintf("cannot parse file [%s] for user [%s] with content type [%s]", header.Filename, user.ID, header.Header.Get("Content-Type"))))
110108

111109
result := url.Values{}
112110
result.Add("document", fmt.Sprintf("The file [%s] is not a valid CSV or Excel file.", header.Filename))
113-
return nil, "", result
111+
return nil, result
114112
}
115113

116114
func (v *BulkMessageHandlerValidator) parseXlsx(ctxLogger telemetry.Logger, user *entities.User, header *multipart.FileHeader) ([]*requests.BulkMessage, url.Values) {

web/pages/bulk-messages/index.vue

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -251,10 +251,9 @@ export default Vue.extend({
251251
this.$store
252252
.dispatch('sendBulkMessages', this.formFile)
253253
.then(() => {
254-
setTimeout(() => {
255-
this.loading = false
256-
this.$router.push({ name: 'threads' })
257-
}, 2000)
254+
this.loading = false
255+
this.formFile = null
256+
this.fetchBulkOrders()
258257
})
259258
.catch((error: AxiosError<ResponsesUnprocessableEntity>) => {
260259
this.errorTitle = capitalize(

0 commit comments

Comments
 (0)