git.frostfs.info/TrueCloudLab/frostfs-sdk-go@v0.0.0-20241022124111-5361f0ecebd3/object/patcher/patcher.go (about) 1 package patcher 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 9 objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" 10 "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/transformer" 11 ) 12 13 var ( 14 ErrOffsetExceedsSize = errors.New("patch offset exceeds object size") 15 ErrInvalidPatchOffsetOrder = errors.New("invalid patch offset order") 16 ErrPayloadPatchIsNil = errors.New("nil payload patch") 17 ErrAttrPatchAlreadyApplied = errors.New("attribute patch already applied") 18 ) 19 20 // PatchRes is the result of patch application. 21 type PatchRes struct { 22 AccessIdentifiers *transformer.AccessIdentifiers 23 } 24 25 // PatchApplier is the interface that provides method to apply header and payload patches. 26 type PatchApplier interface { 27 // ApplyAttributesPatch applies the patch only for the object's attributes. 28 // 29 // ApplyAttributesPatch can't be invoked few times, otherwise it returns `ErrAttrPatchAlreadyApplied` error. 30 // 31 // The call is idempotent for the original header if it's invoked with empty `newAttrs` and 32 // `replaceAttrs = false`. 33 ApplyAttributesPatch(ctx context.Context, newAttrs []objectSDK.Attribute, replaceAttrs bool) error 34 35 // ApplyPayloadPatch applies the patch for the object's payload. 36 // 37 // ApplyPayloadPatch returns `ErrPayloadPatchIsNil` error if patch is nil. 38 ApplyPayloadPatch(ctx context.Context, payloadPatch *objectSDK.PayloadPatch) error 39 40 // Close closes PatchApplier when the patch stream is over. 41 Close(context.Context) (PatchRes, error) 42 } 43 44 // RangeProvider is the interface that provides a method to get original object payload 45 // by a given range. 46 type RangeProvider interface { 47 // GetRange reads an original object payload by the given range. 48 // The method returns io.Reader over the data range only. This means if the data is read out, 49 // then GetRange has to be invoked to provide reader over the next range. 50 GetRange(ctx context.Context, rng *objectSDK.Range) io.Reader 51 } 52 53 type patcher struct { 54 rangeProvider RangeProvider 55 56 objectWriter transformer.ChunkedObjectWriter 57 58 currOffset uint64 59 60 originalPayloadSize uint64 61 62 hdr *objectSDK.Object 63 64 attrPatchAlreadyApplied bool 65 66 readerBuffSize int 67 } 68 69 const ( 70 DefaultReaderBufferSize = 64 * 1024 71 ) 72 73 // Params is parameters to initialize patcher. 74 type Params struct { 75 // Original object header. 76 Header *objectSDK.Object 77 78 // Range provider. 79 RangeProvider RangeProvider 80 81 // ObjectWriter is the writer that writes the patched object. 82 ObjectWriter transformer.ChunkedObjectWriter 83 84 // The size of the buffer used by the original payload range reader. 85 // If it's set to <=0, then `DefaultReaderBufferSize` is used. 86 ReaderBufferSize int 87 } 88 89 func New(prm Params) PatchApplier { 90 readerBufferSize := prm.ReaderBufferSize 91 if readerBufferSize <= 0 { 92 readerBufferSize = DefaultReaderBufferSize 93 } 94 95 return &patcher{ 96 rangeProvider: prm.RangeProvider, 97 98 objectWriter: prm.ObjectWriter, 99 100 hdr: prm.Header, 101 102 originalPayloadSize: prm.Header.PayloadSize(), 103 104 readerBuffSize: readerBufferSize, 105 } 106 } 107 108 func (p *patcher) ApplyAttributesPatch(ctx context.Context, newAttrs []objectSDK.Attribute, replaceAttrs bool) error { 109 defer func() { 110 p.attrPatchAlreadyApplied = true 111 }() 112 113 if p.attrPatchAlreadyApplied { 114 return ErrAttrPatchAlreadyApplied 115 } 116 117 if replaceAttrs { 118 p.hdr.SetAttributes(newAttrs...) 119 } else if len(newAttrs) > 0 { 120 mergedAttrs := mergeAttributes(newAttrs, p.hdr.Attributes()) 121 p.hdr.SetAttributes(mergedAttrs...) 122 } 123 124 if err := p.objectWriter.WriteHeader(ctx, p.hdr); err != nil { 125 return fmt.Errorf("writer header: %w", err) 126 } 127 return nil 128 } 129 130 func (p *patcher) ApplyPayloadPatch(ctx context.Context, payloadPatch *objectSDK.PayloadPatch) error { 131 if payloadPatch == nil { 132 return ErrPayloadPatchIsNil 133 } 134 135 if payloadPatch.Range.GetOffset() < p.currOffset { 136 return fmt.Errorf("%w: current = %d, previous = %d", ErrInvalidPatchOffsetOrder, payloadPatch.Range.GetOffset(), p.currOffset) 137 } 138 139 if payloadPatch.Range.GetOffset() > p.originalPayloadSize { 140 return fmt.Errorf("%w: offset = %d, object size = %d", ErrOffsetExceedsSize, payloadPatch.Range.GetOffset(), p.originalPayloadSize) 141 } 142 143 var err error 144 if p.currOffset, err = p.applyPatch(ctx, payloadPatch, p.currOffset); err != nil { 145 return fmt.Errorf("apply patch: %w", err) 146 } 147 148 return nil 149 } 150 151 func (p *patcher) Close(ctx context.Context) (PatchRes, error) { 152 rng := new(objectSDK.Range) 153 rng.SetOffset(p.currOffset) 154 rng.SetLength(p.originalPayloadSize - p.currOffset) 155 156 // copy remaining originial payload 157 if err := p.copyRange(ctx, rng); err != nil { 158 return PatchRes{}, fmt.Errorf("copy payload: %w", err) 159 } 160 161 aid, err := p.objectWriter.Close(ctx) 162 if err != nil { 163 return PatchRes{}, fmt.Errorf("close object writer: %w", err) 164 } 165 166 return PatchRes{ 167 AccessIdentifiers: aid, 168 }, nil 169 } 170 171 func (p *patcher) copyRange(ctx context.Context, rng *objectSDK.Range) error { 172 rdr := p.rangeProvider.GetRange(ctx, rng) 173 for { 174 buffOrigPayload := make([]byte, p.readerBuffSize) 175 n, readErr := rdr.Read(buffOrigPayload) 176 if readErr != nil { 177 if readErr != io.EOF { 178 return fmt.Errorf("read: %w", readErr) 179 } 180 } 181 _, wrErr := p.objectWriter.Write(ctx, buffOrigPayload[:n]) 182 if wrErr != nil { 183 return fmt.Errorf("write: %w", wrErr) 184 } 185 if readErr == io.EOF { 186 break 187 } 188 } 189 return nil 190 } 191 192 func (p *patcher) applyPatch(ctx context.Context, payloadPatch *objectSDK.PayloadPatch, offset uint64) (newOffset uint64, err error) { 193 newOffset = offset 194 195 // write the original payload chunk before the start of the patch 196 if payloadPatch.Range.GetOffset() > offset { 197 rng := new(objectSDK.Range) 198 rng.SetOffset(offset) 199 rng.SetLength(payloadPatch.Range.GetOffset() - offset) 200 201 if err = p.copyRange(ctx, rng); err != nil { 202 err = fmt.Errorf("copy payload: %w", err) 203 return 204 } 205 206 newOffset = payloadPatch.Range.GetOffset() 207 } 208 209 // apply patch 210 if _, err = p.objectWriter.Write(ctx, payloadPatch.Chunk); err != nil { 211 return 212 } 213 214 if payloadPatch.Range.GetLength() > 0 { 215 newOffset += payloadPatch.Range.GetLength() 216 } 217 218 return 219 } 220 221 func mergeAttributes(newAttrs, oldAttrs []objectSDK.Attribute) []objectSDK.Attribute { 222 attrMap := make(map[string]string, len(newAttrs)) 223 224 for _, attr := range newAttrs { 225 attrMap[attr.Key()] = attr.Value() 226 } 227 228 for i := range oldAttrs { 229 newVal, ok := attrMap[oldAttrs[i].Key()] 230 if !ok { 231 continue 232 } 233 oldAttrs[i].SetValue(newVal) 234 delete(attrMap, oldAttrs[i].Key()) 235 } 236 237 for _, newAttr := range newAttrs { 238 if _, ok := attrMap[newAttr.Key()]; ok { 239 oldAttrs = append(oldAttrs, newAttr) 240 } 241 } 242 243 return oldAttrs 244 }