/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch 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 org.elasticsearch.index.snapshots.blobstore; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.Version; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; import org.elasticsearch.common.lucene.Lucene; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.store.StoreFileMetaData; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Shard snapshot metadata */ public class BlobStoreIndexShardSnapshot implements ToXContent { /** * Information about snapshotted file */ public static class FileInfo { private static final String UNKNOWN_CHECKSUM = "_na_"; private final String name; private final ByteSizeValue partSize; private final long partBytes; private final long numberOfParts; private final StoreFileMetaData metadata; /** * Constructs a new instance of file info * * @param name file name as stored in the blob store * @param metaData the files meta data * @param partSize size of the single chunk */ public FileInfo(String name, StoreFileMetaData metaData, ByteSizeValue partSize) { this.name = name; this.metadata = metaData; long partBytes = Long.MAX_VALUE; if (partSize != null) { partBytes = partSize.getBytes(); } long totalLength = metaData.length(); long numberOfParts = totalLength / partBytes; if (totalLength % partBytes > 0) { numberOfParts++; } if (numberOfParts == 0) { numberOfParts++; } this.numberOfParts = numberOfParts; this.partSize = partSize; this.partBytes = partBytes; } /** * Returns the base file name * * @return file name */ public String name() { return name; } /** * Returns part name if file is stored as multiple parts * * @param part part number * @return part name */ public String partName(long part) { if (numberOfParts > 1) { return name + ".part" + part; } else { return name; } } /** * Returns base file name from part name * * @param blobName part name * @return base file name */ public static String canonicalName(String blobName) { if (blobName.contains(".part")) { return blobName.substring(0, blobName.indexOf(".part")); } return blobName; } /** * Returns original file name * * @return original file name */ public String physicalName() { return metadata.name(); } /** * File length * * @return file length */ public long length() { return metadata.length(); } /** * Returns part size * * @return part size */ public ByteSizeValue partSize() { return partSize; } /** * Returns the size (in bytes) of a given part * * @return the size (in bytes) of a given part */ public long partBytes(int part) { if (numberOfParts == 1) { return length(); } // First and last-but-one parts have a size equal to partBytes if (part < (numberOfParts - 1)) { return partBytes; } // Last part size is deducted from the length and the number of parts return length() - (partBytes * (numberOfParts-1)); } /** * Returns number of parts * * @return number of parts */ public long numberOfParts() { return numberOfParts; } /** * Returns file md5 checksum provided by {@link org.elasticsearch.index.store.Store} * * @return file checksum */ public String checksum() { return metadata.checksum(); } /** * Returns the StoreFileMetaData for this file info. */ public StoreFileMetaData metadata() { return metadata; } /** * Checks if a file in a store is the same file * * @param md file in a store * @return true if file in a store this this file have the same checksum and length */ public boolean isSame(StoreFileMetaData md) { return metadata.isSame(md); } /** * Checks if a file in a store is the same file * * @param fileInfo file in a store * @return true if file in a store this this file have the same checksum and length */ public boolean isSame(FileInfo fileInfo) { if (numberOfParts != fileInfo.numberOfParts) { return false; } if (partBytes != fileInfo.partBytes) { return false; } if (!name.equals(fileInfo.name)) { return false; } if (partSize != null) { if (!partSize.equals(fileInfo.partSize)) { return false; } } else { if (fileInfo.partSize != null) { return false; } } return metadata.isSame(fileInfo.metadata); } /** * Checks if the checksum for the file is unknown. This only is possible on an empty shard's * segments_N file which was created in older Lucene versions. */ public boolean hasUnknownChecksum() { return metadata.checksum().equals(UNKNOWN_CHECKSUM); } static final String NAME = "name"; static final String PHYSICAL_NAME = "physical_name"; static final String LENGTH = "length"; static final String CHECKSUM = "checksum"; static final String PART_SIZE = "part_size"; static final String WRITTEN_BY = "written_by"; static final String META_HASH = "meta_hash"; /** * Serializes file info into JSON * * @param file file info * @param builder XContent builder * @param params parameters */ public static void toXContent(FileInfo file, XContentBuilder builder, ToXContent.Params params) throws IOException { builder.startObject(); builder.field(NAME, file.name); builder.field(PHYSICAL_NAME, file.metadata.name()); builder.field(LENGTH, file.metadata.length()); if (file.metadata.checksum().equals(UNKNOWN_CHECKSUM) == false) { builder.field(CHECKSUM, file.metadata.checksum()); } if (file.partSize != null) { builder.field(PART_SIZE, file.partSize.getBytes()); } if (file.metadata.writtenBy() != null) { builder.field(WRITTEN_BY, file.metadata.writtenBy()); } if (file.metadata.hash() != null && file.metadata().hash().length > 0) { builder.field(META_HASH, file.metadata.hash()); } builder.endObject(); } /** * Parses JSON that represents file info * * @param parser parser * @return file info */ public static FileInfo fromXContent(XContentParser parser) throws IOException { XContentParser.Token token = parser.currentToken(); String name = null; String physicalName = null; long length = -1; String checksum = null; ByteSizeValue partSize = null; Version writtenBy = null; String writtenByStr = null; BytesRef metaHash = new BytesRef(); if (token == XContentParser.Token.START_OBJECT) { while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { String currentFieldName = parser.currentName(); token = parser.nextToken(); if (token.isValue()) { if (NAME.equals(currentFieldName)) { name = parser.text(); } else if (PHYSICAL_NAME.equals(currentFieldName)) { physicalName = parser.text(); } else if (LENGTH.equals(currentFieldName)) { length = parser.longValue(); } else if (CHECKSUM.equals(currentFieldName)) { checksum = parser.text(); } else if (PART_SIZE.equals(currentFieldName)) { partSize = new ByteSizeValue(parser.longValue()); } else if (WRITTEN_BY.equals(currentFieldName)) { writtenByStr = parser.text(); writtenBy = Lucene.parseVersionLenient(writtenByStr, null); } else if (META_HASH.equals(currentFieldName)) { metaHash.bytes = parser.binaryValue(); metaHash.offset = 0; metaHash.length = metaHash.bytes.length; } else { throw new ElasticsearchParseException("unknown parameter [{}]", currentFieldName); } } else { throw new ElasticsearchParseException("unexpected token [{}]", token); } } else { throw new ElasticsearchParseException("unexpected token [{}]",token); } } } // Verify that file information is complete if (name == null || Strings.validFileName(name) == false) { throw new ElasticsearchParseException("missing or invalid file name [" + name + "]"); } else if (physicalName == null || Strings.validFileName(physicalName) == false) { throw new ElasticsearchParseException("missing or invalid physical file name [" + physicalName + "]"); } else if (length < 0) { throw new ElasticsearchParseException("missing or invalid file length"); } else if (writtenBy == null) { throw new ElasticsearchParseException("missing or invalid written_by [" + writtenByStr + "]"); } else if (checksum == null) { throw new ElasticsearchParseException("missing checksum for name [" + name + "]"); } return new FileInfo(name, new StoreFileMetaData(physicalName, length, checksum, writtenBy, metaHash), partSize); } @Override public String toString() { return "[name: " + name + ", numberOfParts: " + numberOfParts + ", partSize: " + partSize + ", partBytes: " + partBytes + ", metadata: " + metadata + "]"; } } private final String snapshot; private final long indexVersion; private final long startTime; private final long time; private final int numberOfFiles; private final long totalSize; private final List<FileInfo> indexFiles; /** * Constructs new shard snapshot metadata from snapshot metadata * * @param snapshot snapshot id * @param indexVersion index version * @param indexFiles list of files in the shard * @param startTime snapshot start time * @param time snapshot running time * @param numberOfFiles number of files that where snapshotted * @param totalSize total size of all files snapshotted */ public BlobStoreIndexShardSnapshot(String snapshot, long indexVersion, List<FileInfo> indexFiles, long startTime, long time, int numberOfFiles, long totalSize) { assert snapshot != null; assert indexVersion >= 0; this.snapshot = snapshot; this.indexVersion = indexVersion; this.indexFiles = Collections.unmodifiableList(new ArrayList<>(indexFiles)); this.startTime = startTime; this.time = time; this.numberOfFiles = numberOfFiles; this.totalSize = totalSize; } /** * Special constructor for the prototype */ private BlobStoreIndexShardSnapshot() { this.snapshot = ""; this.indexVersion = 0; this.indexFiles = Collections.emptyList(); this.startTime = 0; this.time = 0; this.numberOfFiles = 0; this.totalSize = 0; } /** * Returns index version * * @return index version */ public long indexVersion() { return indexVersion; } /** * Returns snapshot id * * @return snapshot id */ public String snapshot() { return snapshot; } /** * Returns list of files in the shard * * @return list of files */ public List<FileInfo> indexFiles() { return indexFiles; } /** * Returns snapshot start time */ public long startTime() { return startTime; } /** * Returns snapshot running time */ public long time() { return time; } /** * Returns number of files that where snapshotted */ public int numberOfFiles() { return numberOfFiles; } /** * Returns total size of all files that where snapshotted */ public long totalSize() { return totalSize; } private static final String NAME = "name"; private static final String INDEX_VERSION = "index_version"; private static final String START_TIME = "start_time"; private static final String TIME = "time"; private static final String NUMBER_OF_FILES = "number_of_files"; private static final String TOTAL_SIZE = "total_size"; private static final String FILES = "files"; private static final ParseField PARSE_NAME = new ParseField("name"); private static final ParseField PARSE_INDEX_VERSION = new ParseField("index_version", "index-version"); private static final ParseField PARSE_START_TIME = new ParseField("start_time"); private static final ParseField PARSE_TIME = new ParseField("time"); private static final ParseField PARSE_NUMBER_OF_FILES = new ParseField("number_of_files"); private static final ParseField PARSE_TOTAL_SIZE = new ParseField("total_size"); private static final ParseField PARSE_FILES = new ParseField("files"); /** * Serializes shard snapshot metadata info into JSON * * @param builder XContent builder * @param params parameters */ @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.field(NAME, snapshot); builder.field(INDEX_VERSION, indexVersion); builder.field(START_TIME, startTime); builder.field(TIME, time); builder.field(NUMBER_OF_FILES, numberOfFiles); builder.field(TOTAL_SIZE, totalSize); builder.startArray(FILES); for (FileInfo fileInfo : indexFiles) { FileInfo.toXContent(fileInfo, builder, params); } builder.endArray(); return builder; } /** * Parses shard snapshot metadata * * @param parser parser * @return shard snapshot metadata */ public static BlobStoreIndexShardSnapshot fromXContent(XContentParser parser) throws IOException { String snapshot = null; long indexVersion = -1; long startTime = 0; long time = 0; int numberOfFiles = 0; long totalSize = 0; List<FileInfo> indexFiles = new ArrayList<>(); if (parser.currentToken() == null) { // fresh parser? move to the first token parser.nextToken(); } XContentParser.Token token = parser.currentToken(); if (token == XContentParser.Token.START_OBJECT) { while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { String currentFieldName = parser.currentName(); token = parser.nextToken(); if (token.isValue()) { if (PARSE_NAME.match(currentFieldName)) { snapshot = parser.text(); } else if (PARSE_INDEX_VERSION.match(currentFieldName)) { // The index-version is needed for backward compatibility with v 1.0 indexVersion = parser.longValue(); } else if (PARSE_START_TIME.match(currentFieldName)) { startTime = parser.longValue(); } else if (PARSE_TIME.match(currentFieldName)) { time = parser.longValue(); } else if (PARSE_NUMBER_OF_FILES.match(currentFieldName)) { numberOfFiles = parser.intValue(); } else if (PARSE_TOTAL_SIZE.match(currentFieldName)) { totalSize = parser.longValue(); } else { throw new ElasticsearchParseException("unknown parameter [{}]", currentFieldName); } } else if (token == XContentParser.Token.START_ARRAY) { if (PARSE_FILES.match(currentFieldName)) { while ((parser.nextToken()) != XContentParser.Token.END_ARRAY) { indexFiles.add(FileInfo.fromXContent(parser)); } } else { throw new ElasticsearchParseException("unknown parameter [{}]", currentFieldName); } } else { throw new ElasticsearchParseException("unexpected token [{}]", token); } } else { throw new ElasticsearchParseException("unexpected token [{}]", token); } } } return new BlobStoreIndexShardSnapshot(snapshot, indexVersion, Collections.unmodifiableList(indexFiles), startTime, time, numberOfFiles, totalSize); } }