Get a quote

معمارية رفع الملفات في SaaS: Presigned URLs لـ S3 ورفع متعدد الأجزاء والتوصيل عبر CDN في Go

رفع الملفات يبدو بسيطًا حتى تتعامل مع ملفات ضخمة ومستأجرين متعددين وعزل التكاليف والتوصيل عبر CDN. هذا هو الأسلوب الصحيح لبنائه لمنتجات SaaS.

رفع الملفات يبدو بسيطًا حتى تتعامل مع ملفات ضخمة ومستأجرين متعددين وعزل التكاليف والتوصيل عبر CDN. توجيه رفع الملفات عبر خلفية Go يبدو واضحًا، لكنه يعني أن كل ملف يمر بخادمك مستهلكًا النطاق الترددي والحوسبة التي تدفع مقابلها. المعمارية الصحيحة لـ SaaS هي الرفع المباشر إلى S3 عبر Presigned URLs مع بادئات مُعزولة لكل مستأجر وCloudFront للتوصيل.

لماذا لا توجه الرفع عبر الخلفية؟

النهج الساذج: العميل يرسل الملف إلى Go API، والـ API يكتبه إلى S3. هذا يعمل للملفات الصغيرة والحجم المنخفض. المشاكل تظهر مع التوسع:

  • كل رفع يشغل goroutine ونطاقًا ترددًا على ECS task
  • رفع الملفات الضخمة يمكن أن يستنفد حدود الاتصال على Fargate tasks الصغيرة
  • تدفع مقابل حركة المرور الواردة إلى ALB والصادرة من ECS إلى S3

مع Presigned URLs، يرفع العميل مباشرة إلى S3 من المتصفح أو التطبيق المحمول. خلفية Go تولد الرابط، تتحقق من اكتمال الرفع، وتسجل البيانات الوصفية. الخادم لا يرى بيانات الملف أبدًا.

تدفق Presigned URL

التدفق الكامل لرفع ملف واحد:

  1. العميل يطلب رابط رفع من Go API
  2. Go يولد Presigned PUT URL مع مفتاح S3 محدد مسبقًا
  3. العميل يرفع الملف مباشرة إلى S3
  4. Go يسجل البيانات الوصفية في PostgreSQL
func (s *FileService) GenerateUploadURL(
  ctx context.Context,
  workspaceID string,
  req UploadURLRequest,
) (UploadURLResponse, error) {
  if req.SizeBytes > s.maxFileSizeBytes {
    return UploadURLResponse{}, ErrFileTooLarge
  }

  fileID := uuid.New().String()
  ext := filepath.Ext(req.Filename)
  objectKey := fmt.Sprintf("workspaces/%s/files/%s%s",
    workspaceID, fileID, ext)

  presignClient := s3.NewPresignClient(s.s3Client)
  presigned, err := presignClient.PresignPutObject(ctx,
    &s3.PutObjectInput{
      Bucket:        aws.String(s.bucket),
      Key:           aws.String(objectKey),
      ContentType:   aws.String(req.ContentType),
      ContentLength: aws.Int64(req.SizeBytes),
    },
    func(opts *s3.PresignOptions) { opts.Expires = 15 * time.Minute },
  )
  if err != nil {
    return UploadURLResponse{}, fmt.Errorf("presign: %w", err)
  }

  return UploadURLResponse{
    UploadURL: presigned.URL,
    ObjectKey: objectKey,
    ExpiresIn: 900,
  }, nil
}

عزل المستأجرين بادئات مفاتيح S3

بنية المفتاح workspaces/{workspace_id}/files/{file_id}.ext مقصودة. توفر عزل المستأجرين على مستوى التخزين. لا تسمح للعملاء باختيار مفتاح S3 الخاص بهم. المفتاح يُولد من جانب الخادم، مرتبط بالـ workspace المصادق عليه، ومسجل قبل بدء الرفع. هذا يمنع هجمات اجتياز المسار.

التحقق من اكتمال الرفع ومعالجة الإخفاقات

بعد رفع العميل إلى S3، تحتاج خلفيتك لمعرفة نجاح الرفع. خياران:

الخيار الأول: العميل يؤكد للـ API بعد الرفع

func (s *FileService) ConfirmUpload(
  ctx context.Context,
  workspaceID, fileID string,
) error {
  _, err := s.s3Client.HeadObject(ctx, &s3.HeadObjectInput{
    Bucket: aws.String(s.bucket),
    Key:    aws.String(objectKey),
  })
  if err != nil {
    return ErrUploadNotFound
  }
  return s.queries.ConfirmUpload(ctx, db.ConfirmUploadParams{
    ID:     fileID,
    Status: "completed",
  })
}

الخيار الثاني: S3 Event Notification إلى SQS

قم بإعداد S3 لإرسال أحداث s3:ObjectCreated:* إلى قائمة انتظار SQS. عامل Go في الخلفية يعالج هذه الأحداث. هذا النهج أكثر متانة لأنه يتعامل مع الحالات التي يقطع فيها العميل الاتصال بعد الرفع إلى S3 لكن قبل استدعاء نقطة التأكيد.

الرفع متعدد الأجزاء للملفات الضخمة

الملفات التي تزيد عن 100 ميغابايت يجب استخدام S3 Multipart Upload. العميل يقسم الملف إلى أجزاء، يرفع كل جزء بـ Presigned URL منفصل، ثم يكمل الرفع.

التوصيل عبر CDN مع CloudFront

لتوصيل الملفات للمستخدمين، لا تولد روابط S3 عامة أبدًا. استخدم CloudFront مع Origin Access Control (OAC):

signer := sign.NewURLSigner(s.cfKeyID, s.cfPrivateKey)
signedURL, err := signer.Sign(
  fmt.Sprintf("https://%s/%s", s.cdnDomain, file.ObjectKey),
  time.Now().Add(1*time.Hour),
)

روابط CloudFront الموقعة تنتهي صلاحيتها ولا يمكن مشاركتها علنًا. هذا مهم لعزل بيانات المستأجرين.

الدروس الرئيسية

الرفع المباشر إلى S3 يقلل بشكل كبير من متطلبات حجم ECS task وتكاليف الشبكة. نظام إدارة مطعم يتعامل مع صور القوائم وملفات PDF للفواتير لا يحتاج Fargate tasks ضخمة عندما تتجاوز عمليات الرفع الخلفية بالكامل.

الخطر التشغيلي الرئيسي هو تراكم سجلات الرفع المعلقة في قاعدة البيانات. عامل التنظيف ليس اختياريًا.

هل تريد بناء نظام معالجة ملفات صحيح لمنتج SaaS الخاص بك؟

Voxire تصمم وتبني أنظمة الخلفية لمنتجات SaaS في لبنان ومنطقة MENA. إذا كان رفع الملفات أو التخزين أو التوصيل عبر CDN عنق زجاجة في منتجك، يمكننا مساعدتك في تصميم المعمارية الصحيحة.

https://voxire.com/get-a-quote/

العودة إلى المدونة
Chat on WhatsApp