001/* 002 * #%L 003 * Netarchivesuite - archive 004 * %% 005 * Copyright (C) 2005 - 2018 The Royal Danish Library, 006 * the National Library of France and the Austrian National Library. 007 * %% 008 * This program is free software: you can redistribute it and/or modify 009 * it under the terms of the GNU Lesser General Public License as 010 * published by the Free Software Foundation, either version 2.1 of the 011 * License, or (at your option) any later version. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Lesser Public License for more details. 017 * 018 * You should have received a copy of the GNU General Lesser Public 019 * License along with this program. If not, see 020 * <http://www.gnu.org/licenses/lgpl-2.1.html>. 021 * #L% 022 */ 023package dk.netarkivet.archive.arcrepositoryadmin; 024 025import java.io.File; 026import java.io.FileWriter; 027import java.io.IOException; 028import java.io.PrintWriter; 029import java.util.Map; 030 031import org.slf4j.Logger; 032import org.slf4j.LoggerFactory; 033 034import dk.netarkivet.archive.arcrepository.distribute.StoreMessage; 035import dk.netarkivet.common.distribute.arcrepository.ReplicaStoreState; 036import dk.netarkivet.common.exceptions.ArgumentNotValid; 037import dk.netarkivet.common.exceptions.IOFailure; 038import dk.netarkivet.common.exceptions.PermissionDenied; 039import dk.netarkivet.common.exceptions.UnknownID; 040 041/** 042 * Class for accessing and manipulating the administrative data for the ArcRepository. In the current implementation, it 043 * consists of a file with a number of lines of the form: filename checksum state timestamp-for-last-state-change [, 044 * bitarchive> storestatus timestamp-for-last-state-change]* 045 * <p> 046 * If a line in the admin data file is corrupt, the entry is removed from admindata. 047 * <p> 048 * Notes: If the admindata file does not exist on start-up, the file is created in the constructor. If the admindata 049 * file on start-up is the oldversion, the admindata file is migrated to the new version. 050 * 051 * @deprecated Use the database instance instead, DatabaseAdmin. 052 */ 053@Deprecated 054public class UpdateableAdminData extends AdminData implements Admin { 055 056 /** Logger for this class. */ 057 private static final Logger log = LoggerFactory.getLogger(UpdateableAdminData.class); 058 059 /** the singleton for the UpdateableAdminData class. */ 060 private static UpdateableAdminData instance; 061 062 /** 063 * Constructor for the UpdateableAdminData class. Reads the admindata file if it exists, creates it otherwise 064 * 065 * @throws PermissionDenied if admin data directory is not accessible 066 * @throws IOFailure if there is trouble reading or creating the admin data file 067 */ 068 private UpdateableAdminData() throws IOFailure, PermissionDenied { 069 super(); 070 if (!adminDataFile.exists()) { 071 log.info("Creating new admin data file {}", adminDataFile.getAbsolutePath()); 072 } 073 // Always rewrite after read, as we're cutting out old entries 074 // to shorten the file. 075 write(); 076 log.debug("AdminData created"); 077 } 078 079 /** 080 * Get the singleton instance. 081 * 082 * @return The singleton 083 */ 084 public static UpdateableAdminData getInstance() { 085 if (UpdateableAdminData.instance == null) { 086 UpdateableAdminData.instance = new UpdateableAdminData(); 087 } 088 return UpdateableAdminData.instance; 089 } 090 091 /** 092 * Add new entry to the admin data, and persist it to disk. 093 * 094 * @param filename A filename 095 * @param replyInfo A replyInfo for this entry (may be null) 096 * @param checksum The Checksum for this file 097 */ 098 public void addEntry(String filename, StoreMessage replyInfo, String checksum) { 099 addEntry(filename, replyInfo, checksum, true); 100 } 101 102 /** 103 * Add new entry to the admin data, and persist it to disk, if persistNow set to true. 104 * 105 * @param filename A filename 106 * @param replyInfo A replyInfo for this entry (may be null) 107 * @param checksum The Checksum for this file 108 * @param persistNow Shall we persist this entry now? 109 */ 110 public void addEntry(String filename, StoreMessage replyInfo, String checksum, boolean persistNow) { 111 ArgumentNotValid.checkNotNullOrEmpty(filename, "String filename"); 112 ArgumentNotValid.checkNotNullOrEmpty(checksum, "String checksum"); 113 storeEntries.put(filename, new ArcRepositoryEntry(filename, checksum, replyInfo)); 114 if (persistNow) { 115 // Persist the new entry 116 // Note: This appends the new entry to the end of the admindata file 117 write(filename); 118 } 119 } 120 121 /** 122 * Records the replyInfo (StoreMessage object) so that it can be retrieved using the given file name. 123 * 124 * @param fileName An arc file that someone is trying to store. 125 * @param replyInfo A StoreMessage object related to this filename. 126 * @throws UnknownID if no info has been registered for the filename. 127 */ 128 public void setReplyInfo(String fileName, StoreMessage replyInfo) throws UnknownID { 129 ArgumentNotValid.checkNotNullOrEmpty(fileName, "String fileName"); 130 ArgumentNotValid.checkNotNull(replyInfo, "replyInfo"); 131 if (!hasEntry(fileName)) { 132 throw new UnknownID("Cannot set replyinfo '" + replyInfo + "' for unregistered file '" + fileName + "'"); 133 } 134 ArcRepositoryEntry entry = storeEntries.get(fileName); 135 entry.setReplyInfo(replyInfo); // TODO Should this be persisted 136 } 137 138 /** 139 * Removes the replyInfo associated with arcfileName. 140 * 141 * @param fileName A file that we are trying to store. 142 * @return the replyInfo associated with arcfileName. 143 * @throws UnknownID If the filename is not known. or no replyInfo is associated with arcfileName. 144 */ 145 public StoreMessage removeReplyInfo(String fileName) throws UnknownID { 146 ArgumentNotValid.checkNotNullOrEmpty(fileName, "String fileName"); 147 if (!hasEntry(fileName)) { 148 throw new UnknownID("Cannot get reply info for unregistered file '" + fileName + "'"); 149 } 150 if (!hasReplyInfo(fileName)) { 151 throw new UnknownID("replyInfo not set for " + fileName); 152 } 153 ArcRepositoryEntry entry = storeEntries.get(fileName); 154 return entry.getAndRemoveReplyInfo(); 155 } 156 157 /** 158 * Sets the store state for the given file on the given bitarchive. 159 * 160 * @param fileName A file that is being stored. 161 * @param replicaID A bitarchive. 162 * @param state The state of upload of arcfileName on bitarchiveID. 163 * @throws UnknownID If the file does not have a store entry. 164 * @throws ArgumentNotValid If the arguments are null or empty 165 */ 166 public void setState(String fileName, String replicaID, ReplicaStoreState state) throws UnknownID, ArgumentNotValid { 167 ArgumentNotValid.checkNotNullOrEmpty(fileName, "String fileName"); 168 ArgumentNotValid.checkNotNullOrEmpty(replicaID, "String replicaID"); 169 ArgumentNotValid.checkNotNull(state, "ReplicaStoreState state"); 170 if (!hasEntry(fileName)) { 171 final String message = "Unregistered file '" + fileName + "' cannot be set to state " + state + " in '" 172 + replicaID + "'"; 173 log.warn(message); 174 throw new UnknownID(message); 175 } 176 177 // TODO What is this good for? 178 // Only used by the toString() method. 179 if (!knownBitArchives.contains(replicaID)) { 180 knownBitArchives.add(replicaID); 181 } 182 storeEntries.get(fileName).setStoreState(replicaID, state); 183 write(fileName); // Add entry for arcfileName in persistent storage. 184 } 185 186 /** 187 * Set/update the checksum for a given arcfileName in the admindata. 188 * 189 * @param fileName Unique name of file for which to store checksum 190 * @param checkSum The generated (MD5) checksum to be stored in reference table 191 * @throws UnknownID if the file is not already registered. 192 * @throws ArgumentNotValid If the arcfileName or the checksum is either null or the empty string. 193 */ 194 public void setCheckSum(String fileName, String checkSum) throws ArgumentNotValid, UnknownID { 195 ArgumentNotValid.checkNotNullOrEmpty(fileName, "String fileName"); 196 ArgumentNotValid.checkNotNullOrEmpty(checkSum, "String checkSum"); 197 if (!hasEntry(fileName)) { 198 throw new UnknownID("Cannot change checksum for unregistered file '" + fileName + "'"); 199 } 200 log.trace("Changing checksum for {} from {} to {}", fileName, getCheckSum(fileName), checkSum); 201 storeEntries.get(fileName).setChecksum(checkSum); 202 write(); // Write everything to persistent storage 203 } 204 205 /** 206 * Write all the admin data to file. This overwrites the previous file and writes data for all entries. This 207 * operation can be rather time-consuming if there is a lot of data. We expect to only do this a) when creating a 208 * new admindata (in order to flush out repeated entries created during uploads) and b) when performing a correct 209 * (to ensure that an arcfile only has one checksum). The write is done atomically, i.e. either the old file is kept 210 * or the entire new file is written. 211 * 212 * @throws IOFailure on trouble writing to file 213 */ 214 private void write() throws IOFailure { 215 // First write admindata to a temporary file. 216 final File adminDataStore = adminDataFile; 217 final File tmpDataStore = new File(adminDir, AdminData.ADMIN_FILE_NAME + ".tmp"); 218 final File backupDataStore = new File(adminDir, AdminData.ADMIN_FILE_NAME + ".backup"); 219 PrintWriter writer = null; 220 try { 221 final FileWriter out = new FileWriter(tmpDataStore); 222 writer = new PrintWriter(out); 223 writer.println(VERSION_NUMBER); 224 for (Map.Entry<String, ArcRepositoryEntry> entry : storeEntries.entrySet()) { 225 final String arcfilename = entry.getKey(); 226 final ArcRepositoryEntry arcrepentry = entry.getValue(); 227 write(writer, arcfilename, arcrepentry); 228 } 229 writer.flush(); 230 writer.close(); 231 writer = null; 232 adminDataStore.renameTo(backupDataStore); 233 tmpDataStore.renameTo(adminDataStore); 234 } catch (IOException e) { 235 throw new IOFailure("Failed to write admin data to '" + adminDataFile.getPath() + "'", e); 236 } finally { 237 if (writer != null) { 238 writer.flush(); 239 writer.close(); 240 } 241 // Delete the temporary file if write failed. 242 tmpDataStore.delete(); 243 if (!adminDataStore.exists()) { 244 backupDataStore.renameTo(adminDataStore); 245 } else { 246 backupDataStore.delete(); 247 } 248 } 249 250 } 251 252 /** 253 * Write a single entry to the admin data file. This uses the ArcRepositoryEntry.output() method. 254 * 255 * @param writer The output stream 256 * @param arcfilename the filename which entry is to be written 257 * @param arcrepentry The data kept for this arcfile 258 * @throws ArgumentNotValid if arcrepentry.getFilename() != arcfilename 259 */ 260 private void write(PrintWriter writer, String arcfilename, final ArcRepositoryEntry arcrepentry) 261 throws ArgumentNotValid { 262 ArgumentNotValid.checkTrue(arcrepentry.getFilename().equals(arcfilename), 263 "arcrepentry.getFilename() is not equal to arcfilename (!!)"); 264 265 arcrepentry.output(writer); 266 writer.println(); 267 } 268 269 /** 270 * Write a particular entry to the admin data file. This will append the data to the end of the file. 271 * 272 * @param filename the name of the file which entry is to written to admin data file 273 * @throws IOFailure If an exception occurs when accessing the file. 274 */ 275 private void write(String filename) throws IOFailure { 276 ArcRepositoryEntry entry = storeEntries.get(filename); 277 File adminDataStore = adminDataFile; 278 PrintWriter writer = null; 279 try { 280 final FileWriter out = new FileWriter(adminDataStore, true); 281 writer = new PrintWriter(out); 282 write(writer, filename, entry); 283 } catch (IOException e) { 284 throw new IOFailure("Failed to write admin data for '" + filename + "' to '" + adminDataFile.getName() 285 + "'", e); 286 } finally { 287 if (writer != null) { 288 writer.flush(); 289 writer.close(); 290 } 291 } 292 log.debug("appending entry for filename '{}' to admin.data", filename); 293 } 294 295 /** Makes sure all data is written to disk. */ 296 public void close() { 297 if (instance != null) { 298 write(); // This rewrites all admindata onto disk 299 } 300 instance = null; 301 } 302 303}