blob: 87debcd20eb1a0d72e28121affde678844683ba4 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package cli
import (
"fmt"
"strings"
"github.com/apache/answer/internal/entity"
"xorm.io/builder"
"xorm.io/xorm"
)
// scanFileRecords visits matching file records in id order, ignoring --limit.
func scanFileRecords(x *xorm.Engine, label string, cond builder.Cond, visit func(*entity.FileRecord) error) error {
return runBatchedFix(newFixRunState(0), func(afterID int64) ([]*entity.FileRecord, int64, error) {
records := make([]*entity.FileRecord, 0)
err := x.Select("id, file_url").Where(builder.Gt{"id": afterID}.And(cond)).OrderBy("id").Limit(fixBatchSize).Find(&records)
if err != nil {
return nil, 0, fmt.Errorf("query %s file_record failed: %w", label, err)
}
var lastID int64
if len(records) > 0 {
lastID = int64(records[len(records)-1].ID)
}
return records, lastID, nil
}, visit)
}
// fixFileRecordBySources ignores --limit and keeps unreferenced records (see
// findUnreferencedRecords) on their old URL.
func fixFileRecordBySources(x *xorm.Engine, opts *FixURLPrefixOptions, sources []string, label string) (fixResult, error) {
held, err := findUnreferencedRecords(x, opts, sources, label)
if err != nil {
return fixResult{}, err
}
result := fixResult{}
state := newFixRunState(0)
err = scanFileRecords(x, label, builder.In("source", sources), func(record *entity.FileRecord) error {
target := fmt.Sprintf("file_record id=%d", record.ID)
if held[record.ID] {
fmt.Printf("[%s] WARN %s: no revision references the new URL yet, keeping: %s\n", label, target, record.FileURL)
result.Skipped++
return nil
}
return applyValueFix(&result, opts, state, label, target, record.FileURL, func(newURL string) error {
_, err := x.ID(record.ID).Cols("file_url").NoAutoTime().Update(&entity.FileRecord{FileURL: newURL})
if err != nil {
return fmt.Errorf("update %s file_record (id=%d) failed: %w", label, record.ID, err)
}
return nil
})
})
if err != nil {
return fixResult{}, err
}
return result, nil
}
// findUnreferencedRecords returns unlinked records whose new URL is in no
// revision yet; the cleaner would delete them. Dry runs assume revisions fixed.
func findUnreferencedRecords(x *xorm.Engine, opts *FixURLPrefixOptions, sources []string, label string) (map[int]bool, error) {
pending := make(map[string][]int) // new URL -> record IDs
maxLen := 0
cond := builder.In("source", sources).And(builder.Eq{"object_id": 0, "status": entity.FileRecordStatusAvailable})
err := scanFileRecords(x, label, cond, func(record *entity.FileRecord) error {
if classifyValue(record.FileURL, opts.SrcPrefix, opts.DstPrefix) == matchFix {
newURL := swapPrefix(record.FileURL, opts.SrcPrefix, opts.DstPrefix)
pending[newURL] = append(pending[newURL], record.ID)
maxLen = max(maxLen, len(newURL))
}
return nil
})
if err != nil || len(pending) == 0 {
return nil, err
}
err = runBatchedFix(newFixRunState(0), func(afterID int64) ([]*entity.Revision, int64, error) {
revisions := make([]*entity.Revision, 0)
err := x.Select("id, content").Where("id > ?", afterID).OrderBy("id").Limit(fixBatchSize).Find(&revisions)
if err != nil {
return nil, 0, fmt.Errorf("query revisions for %s file_record failed: %w", label, err)
}
lastID, err := lastStringID(revisions, func(r *entity.Revision) string { return r.ID })
return revisions, lastID, err
}, func(revision *entity.Revision) error {
content := revision.Content
if opts.DryRun && classifyText(content, opts.SrcPrefix, opts.DstPrefix, jsonURLMarkers) == matchFix {
content = replaceURLPrefix(content, opts.SrcPrefix, opts.DstPrefix, jsonURLMarkers)
}
// Pending URLs start with DST_PREFIX: try each such substring up to maxLen.
for rest := content; ; rest = rest[1:] {
i := strings.Index(rest, opts.DstPrefix)
if i < 0 {
break
}
rest = rest[i:]
for n := len(opts.DstPrefix) + 1; n <= min(maxLen, len(rest)); n++ {
delete(pending, rest[:n])
}
}
return nil
})
if err != nil {
return nil, err
}
held := make(map[int]bool)
for _, ids := range pending {
for _, id := range ids {
held[id] = true
}
}
return held, nil
}
// fileRecordSync moves a source's records off SRC_PREFIX once their value is on
// DST_PREFIX, which also repairs records left behind by earlier runs.
type fileRecordSync struct {
x *xorm.Engine
opts *FixURLPrefixOptions
label string
ids map[string][]int // SRC_PREFIX URL -> record IDs; file_url is not indexed
}
func newFileRecordSync(x *xorm.Engine, opts *FixURLPrefixOptions, label, source string) (*fileRecordSync, error) {
s := &fileRecordSync{x: x, opts: opts, label: label, ids: make(map[string][]int)}
if opts.DryRun {
return s, nil
}
cond := builder.Eq{"source": source}.And(builder.Expr("file_url LIKE ?", opts.SrcPrefix+"%"))
err := scanFileRecords(x, label, cond, func(record *entity.FileRecord) error {
s.ids[record.FileURL] = append(s.ids[record.FileURL], record.ID)
return nil
})
return s, err
}
func (s *fileRecordSync) sync(value string) error {
if s.opts.DryRun || classifyValue(value, s.opts.SrcPrefix, s.opts.DstPrefix) != matchSkip {
return nil
}
oldURL := swapPrefix(value, s.opts.DstPrefix, s.opts.SrcPrefix)
ids, ok := s.ids[oldURL]
if !ok {
return nil
}
_, err := s.x.In("id", ids).Cols("file_url").NoAutoTime().Update(&entity.FileRecord{FileURL: value})
if err != nil {
return fmt.Errorf("update %s file_record (url=%s) failed: %w", s.label, oldURL, err)
}
delete(s.ids, oldURL)
return nil
}