@@ -578,11 +578,17 @@ func (c *Client) UploadSQLiteBundleFiles(ctx context.Context, app, archive strin
578578 if err != nil {
579579 return SQLiteBundleUploadResult {}, err
580580 }
581- validatedParts , err := openValidatedSQLiteBundleFiles (ctx , preparedManifest , parts )
581+ if preparedManifest .SnapshotID == "" {
582+ if err := validateMutableSQLiteBundleFiles (ctx , preparedManifest , parts ); err != nil {
583+ return SQLiteBundleUploadResult {}, err
584+ }
585+ return c .uploadMutableSQLiteBundleFiles (ctx , app , archive , preparedManifest , manifestBody , parts )
586+ }
587+ validatedParts , err := openValidatedSnapshotSQLiteBundleFiles (ctx , preparedManifest , parts )
582588 if err != nil {
583589 return SQLiteBundleUploadResult {}, err
584590 }
585- defer closeValidatedSQLiteBundleFiles (validatedParts )
591+ defer closeValidatedSnapshotSQLiteBundleFiles (validatedParts )
586592 for _ , part := range validatedParts {
587593 _ , uploadErr := c .UploadSQLiteBundlePart (ctx , app , archive , SQLiteBundlePartUpload {
588594 Index : part .part .Index ,
@@ -599,6 +605,53 @@ func (c *Client) UploadSQLiteBundleFiles(ctx context.Context, app, archive strin
599605 return c .uploadSQLiteBundleManifest (ctx , app , archive , manifestBody )
600606}
601607
608+ func (c * Client ) uploadMutableSQLiteBundleFiles (
609+ ctx context.Context ,
610+ app , archive string ,
611+ manifest SQLiteBundleManifest ,
612+ manifestBody []byte ,
613+ parts []SQLiteBundlePartFile ,
614+ ) (SQLiteBundleUploadResult , error ) {
615+ for index , part := range parts {
616+ expected := manifest .Parts [index ]
617+ file , err := os .Open (part .Path )
618+ if err != nil {
619+ return SQLiteBundleUploadResult {}, fmt .Errorf ("open sqlite bundle part %d: %w" , index , err )
620+ }
621+ hash := sha256 .New ()
622+ var streamed byteCounter
623+ _ , uploadErr := c .UploadSQLiteBundlePart (ctx , app , archive , SQLiteBundlePartUpload {
624+ Index : expected .Index ,
625+ Body : io .TeeReader (file , io .MultiWriter (hash , & streamed )),
626+ Size : expected .Size ,
627+ SHA256 : expected .SHA256 ,
628+ Compression : SQLiteGzipCompression ,
629+ })
630+ closeErr := file .Close ()
631+ if uploadErr != nil {
632+ return SQLiteBundleUploadResult {}, uploadErr
633+ }
634+ if closeErr != nil {
635+ return SQLiteBundleUploadResult {}, fmt .Errorf ("close sqlite bundle part %d: %w" , index , closeErr )
636+ }
637+ if int64 (streamed ) != expected .Size ||
638+ ! strings .EqualFold (fmt .Sprintf ("%x" , hash .Sum (nil )), expected .SHA256 ) {
639+ return SQLiteBundleUploadResult {}, fmt .Errorf (
640+ "sqlite bundle part file %d changed during upload" ,
641+ index ,
642+ )
643+ }
644+ }
645+ return c .uploadSQLiteBundleManifest (ctx , app , archive , manifestBody )
646+ }
647+
648+ type byteCounter int64
649+
650+ func (counter * byteCounter ) Write (p []byte ) (int , error ) {
651+ * counter += byteCounter (len (p ))
652+ return len (p ), nil
653+ }
654+
602655func (c * Client ) UploadSQLiteBundleManifest (ctx context.Context , app , archive string , manifest SQLiteBundleManifest ) (SQLiteBundleUploadResult , error ) {
603656 _ , manifestBody , err := prepareSQLiteBundleManifest (app , archive , manifest )
604657 if err != nil {
@@ -642,15 +695,30 @@ func prepareSQLiteBundleManifest(app, archive string, manifest SQLiteBundleManif
642695 return SQLiteBundleManifest {}, nil , fmt .Errorf ("encode sqlite bundle manifest: %w" , err )
643696 }
644697 body := buf .Bytes ()
645- if int64 ( len ( body )) > maxSQLiteBundleManifestBytes {
646- return SQLiteBundleManifest {}, nil , fmt . Errorf (
647- "sqlite bundle manifest must not exceed %d bytes " ,
648- maxSQLiteBundleManifestBytes ,
649- )
698+ if err := validateSQLiteBundleManifestSize (
699+ int64 ( len ( body )),
700+ manifest . SnapshotID != " " ,
701+ ); err != nil {
702+ return SQLiteBundleManifest {}, nil , err
650703 }
651704 return manifest , body , nil
652705}
653706
707+ func sqliteBundleManifestSizeLimit (snapshotScoped bool ) int64 {
708+ if snapshotScoped {
709+ return maxSQLiteSnapshotBundleManifestBytes
710+ }
711+ return maxSQLiteMutableBundleManifestBytes
712+ }
713+
714+ func validateSQLiteBundleManifestSize (size int64 , snapshotScoped bool ) error {
715+ maxBodySize := sqliteBundleManifestSizeLimit (snapshotScoped )
716+ if size > maxBodySize {
717+ return fmt .Errorf ("sqlite bundle manifest must not exceed %d bytes" , maxBodySize )
718+ }
719+ return nil
720+ }
721+
654722func validateSQLiteBundleManifest (manifest SQLiteBundleManifest , app , archive string ) error {
655723 snapshotScoped := manifest .SnapshotID != ""
656724 if snapshotScoped && ! validSQLiteSnapshotID (manifest .SnapshotID ) {
@@ -788,9 +856,15 @@ func validateSQLiteBundlePartLimit(index int, size int64, snapshotScoped bool) e
788856 if snapshotScoped && index >= maxSQLiteBundleParts {
789857 return fmt .Errorf ("sqlite bundle part index must be between 0 and %d" , maxSQLiteBundleParts - 1 )
790858 }
791- maxSize := DefaultSQLiteBundleChunkSize
792- if size <= 0 || size > maxSize {
793- return fmt .Errorf ("sqlite bundle part %d size must be between 1 and %d bytes" , index , maxSize )
859+ if size <= 0 {
860+ return fmt .Errorf ("sqlite bundle part %d size must be positive" , index )
861+ }
862+ if snapshotScoped && size > DefaultSQLiteBundleChunkSize {
863+ return fmt .Errorf (
864+ "sqlite bundle part %d size must be between 1 and %d bytes" ,
865+ index ,
866+ DefaultSQLiteBundleChunkSize ,
867+ )
794868 }
795869 return nil
796870}
@@ -803,7 +877,10 @@ func validateSQLiteBundleManifestLimits(manifest SQLiteBundleManifest) error {
803877 if snapshotScoped && len (manifest .Parts ) > maxSQLiteBundleParts {
804878 return fmt .Errorf ("sqlite bundle manifest must contain between 1 and %d parts" , maxSQLiteBundleParts )
805879 }
806- if manifest .Object .Size <= 0 || manifest .Object .Size > maxSQLiteBundleObjectSize {
880+ if manifest .Object .Size <= 0 {
881+ return fmt .Errorf ("sqlite bundle object size must be positive" )
882+ }
883+ if snapshotScoped && manifest .Object .Size > maxSQLiteBundleObjectSize {
807884 return fmt .Errorf ("sqlite bundle object size must be between 1 and %d bytes" , maxSQLiteBundleObjectSize )
808885 }
809886 if manifest .CompressedObject .Size <= 0 {
@@ -839,31 +916,34 @@ func validateSQLiteBundleManifestLimits(manifest SQLiteBundleManifest) error {
839916 return nil
840917}
841918
842- type validatedSQLiteBundlePartFile struct {
919+ type validatedSnapshotSQLiteBundlePartFile struct {
843920 part SQLiteBundlePart
844921 file * os.File
845922 tempDir string
846923}
847924
848- func openValidatedSQLiteBundleFiles (
925+ func openValidatedSnapshotSQLiteBundleFiles (
849926 ctx context.Context ,
850927 manifest SQLiteBundleManifest ,
851928 parts []SQLiteBundlePartFile ,
852- ) (_ []validatedSQLiteBundlePartFile , err error ) {
929+ ) (_ []validatedSnapshotSQLiteBundlePartFile , err error ) {
853930 if err := validateSQLiteBundleManifest (manifest , manifest .App , manifest .Archive ); err != nil {
854931 return nil , err
855932 }
933+ if manifest .SnapshotID == "" {
934+ return nil , fmt .Errorf ("sqlite bundle snapshot id is required for immutable upload staging" )
935+ }
856936 if len (parts ) != len (manifest .Parts ) {
857937 return nil , fmt .Errorf ("sqlite bundle has %d part files, want %d" , len (parts ), len (manifest .Parts ))
858938 }
859939 tempDir , err := os .MkdirTemp ("" , "crawl-sqlite-upload-*" )
860940 if err != nil {
861941 return nil , fmt .Errorf ("create sqlite bundle upload snapshot: %w" , err )
862942 }
863- validated := make ([]validatedSQLiteBundlePartFile , 0 , len (parts ))
943+ validated := make ([]validatedSnapshotSQLiteBundlePartFile , 0 , len (parts ))
864944 defer func () {
865945 if err != nil {
866- closeValidatedSQLiteBundleFiles (validated )
946+ closeValidatedSnapshotSQLiteBundleFiles (validated )
867947 _ = os .RemoveAll (tempDir )
868948 }
869949 }()
@@ -876,38 +956,44 @@ func openValidatedSQLiteBundleFiles(
876956 if err != nil {
877957 return nil , err
878958 }
879- validated = append (validated , validatedSQLiteBundlePartFile {
959+ validated = append (validated , validatedSnapshotSQLiteBundlePartFile {
880960 part : expected ,
881961 file : snapshot ,
882962 tempDir : tempDir ,
883963 })
884964 }
885- if manifest .SnapshotID != "" {
886- if err := validateSnapshotSQLiteBundleContent (ctx , manifest , validated ); err != nil {
887- return nil , err
888- }
965+ if err := validateSnapshotSQLiteBundleContent (ctx , manifest , validated ); err != nil {
966+ return nil , err
889967 }
890968 return validated , nil
891969}
892970
971+ func validateMutableSQLiteBundleFiles (
972+ ctx context.Context ,
973+ manifest SQLiteBundleManifest ,
974+ parts []SQLiteBundlePartFile ,
975+ ) error {
976+ if len (parts ) != len (manifest .Parts ) {
977+ return fmt .Errorf ("sqlite bundle has %d part files, want %d" , len (parts ), len (manifest .Parts ))
978+ }
979+ for index , part := range parts {
980+ expected := manifest .Parts [index ]
981+ if part .SQLiteBundlePart != expected {
982+ return fmt .Errorf ("sqlite bundle part file %d does not match the manifest" , index )
983+ }
984+ if err := copyValidatedSQLiteBundlePart (ctx , index , part , io .Discard ); err != nil {
985+ return err
986+ }
987+ }
988+ return nil
989+ }
990+
893991func snapshotSQLiteBundlePart (
894992 ctx context.Context ,
895993 tempDir string ,
896994 index int ,
897995 part SQLiteBundlePartFile ,
898996) (_ * os.File , err error ) {
899- source , err := os .Open (part .Path )
900- if err != nil {
901- return nil , fmt .Errorf ("open sqlite bundle part %d: %w" , index , err )
902- }
903- defer func () { _ = source .Close () }()
904- infoBefore , err := source .Stat ()
905- if err != nil {
906- return nil , fmt .Errorf ("stat sqlite bundle part %d: %w" , index , err )
907- }
908- if ! infoBefore .Mode ().IsRegular () || infoBefore .Size () != part .Size {
909- return nil , fmt .Errorf ("sqlite bundle part file %d must be a %d-byte regular file" , index , part .Size )
910- }
911997 snapshotPath := filepath .Join (tempDir , fmt .Sprintf ("part-%04d" , index ))
912998 snapshot , err := os .OpenFile (snapshotPath , os .O_CREATE | os .O_EXCL | os .O_RDWR , 0o600 )
913999 if err != nil {
@@ -918,21 +1004,8 @@ func snapshotSQLiteBundlePart(
9181004 _ = snapshot .Close ()
9191005 }
9201006 }()
921- hash := sha256 .New ()
922- size , err := copyWithContext (ctx , io .MultiWriter (snapshot , hash ), source )
923- if err != nil {
924- return nil , fmt .Errorf ("snapshot sqlite bundle part %d: %w" , index , err )
925- }
926- infoAfter , err := source .Stat ()
927- if err != nil {
928- return nil , fmt .Errorf ("restat sqlite bundle part %d: %w" , index , err )
929- }
930- if ! os .SameFile (infoBefore , infoAfter ) || infoAfter .Size () != part .Size || size != part .Size {
931- return nil , fmt .Errorf ("sqlite bundle part file %d changed during validation" , index )
932- }
933- actualSHA256 := fmt .Sprintf ("%x" , hash .Sum (nil ))
934- if ! strings .EqualFold (actualSHA256 , part .SHA256 ) {
935- return nil , fmt .Errorf ("sqlite bundle part file %d sha256 does not match the manifest" , index )
1007+ if err := copyValidatedSQLiteBundlePart (ctx , index , part , snapshot ); err != nil {
1008+ return nil , err
9361009 }
9371010 if err := snapshot .Sync (); err != nil {
9381011 return nil , fmt .Errorf ("sync sqlite bundle part snapshot %d: %w" , index , err )
@@ -947,10 +1020,47 @@ func snapshotSQLiteBundlePart(
9471020 return snapshot , nil
9481021}
9491022
1023+ func copyValidatedSQLiteBundlePart (
1024+ ctx context.Context ,
1025+ index int ,
1026+ part SQLiteBundlePartFile ,
1027+ dst io.Writer ,
1028+ ) error {
1029+ source , err := os .Open (part .Path )
1030+ if err != nil {
1031+ return fmt .Errorf ("open sqlite bundle part %d: %w" , index , err )
1032+ }
1033+ defer func () { _ = source .Close () }()
1034+ infoBefore , err := source .Stat ()
1035+ if err != nil {
1036+ return fmt .Errorf ("stat sqlite bundle part %d: %w" , index , err )
1037+ }
1038+ if ! infoBefore .Mode ().IsRegular () || infoBefore .Size () != part .Size {
1039+ return fmt .Errorf ("sqlite bundle part file %d must be a %d-byte regular file" , index , part .Size )
1040+ }
1041+ hash := sha256 .New ()
1042+ size , err := copyWithContext (ctx , io .MultiWriter (dst , hash ), source )
1043+ if err != nil {
1044+ return fmt .Errorf ("validate sqlite bundle part %d: %w" , index , err )
1045+ }
1046+ infoAfter , err := source .Stat ()
1047+ if err != nil {
1048+ return fmt .Errorf ("restat sqlite bundle part %d: %w" , index , err )
1049+ }
1050+ if ! os .SameFile (infoBefore , infoAfter ) || infoAfter .Size () != part .Size || size != part .Size {
1051+ return fmt .Errorf ("sqlite bundle part file %d changed during validation" , index )
1052+ }
1053+ actualSHA256 := fmt .Sprintf ("%x" , hash .Sum (nil ))
1054+ if ! strings .EqualFold (actualSHA256 , part .SHA256 ) {
1055+ return fmt .Errorf ("sqlite bundle part file %d sha256 does not match the manifest" , index )
1056+ }
1057+ return nil
1058+ }
1059+
9501060func validateSnapshotSQLiteBundleContent (
9511061 ctx context.Context ,
9521062 manifest SQLiteBundleManifest ,
953- parts []validatedSQLiteBundlePartFile ,
1063+ parts []validatedSnapshotSQLiteBundlePartFile ,
9541064) error {
9551065 compressedHash := sha256 .New ()
9561066 var compressedSize int64
@@ -996,7 +1106,7 @@ func validateSnapshotSQLiteBundleContent(
9961106 return rewindValidatedSQLiteBundleFiles (parts )
9971107}
9981108
999- func rewindValidatedSQLiteBundleFiles (parts []validatedSQLiteBundlePartFile ) error {
1109+ func rewindValidatedSQLiteBundleFiles (parts []validatedSnapshotSQLiteBundlePartFile ) error {
10001110 for index , part := range parts {
10011111 if _ , err := part .file .Seek (0 , io .SeekStart ); err != nil {
10021112 return fmt .Errorf ("rewind sqlite bundle part %d: %w" , index , err )
@@ -1005,7 +1115,7 @@ func rewindValidatedSQLiteBundleFiles(parts []validatedSQLiteBundlePartFile) err
10051115 return nil
10061116}
10071117
1008- func closeValidatedSQLiteBundleFiles (parts []validatedSQLiteBundlePartFile ) {
1118+ func closeValidatedSnapshotSQLiteBundleFiles (parts []validatedSnapshotSQLiteBundlePartFile ) {
10091119 tempDirs := map [string ]struct {}{}
10101120 for _ , part := range parts {
10111121 _ = part .file .Close ()
0 commit comments