001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: https://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: https://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.loader;
029
030import java.io.InputStream;
031import java.io.OutputStream;
032import java.nio.file.Files;
033import java.nio.file.LinkOption;
034import java.nio.file.Path;
035import java.nio.file.Paths;
036import java.util.Comparator;
037import java.util.List;
038import java.util.stream.Collectors;
039import java.util.stream.Stream;
040
041/**
042 * RFS based storage for generated image cache entries.<p>
043 */
044public class CmsRfsImageCache implements I_CmsImageCache {
045
046    /** The image cache repository. */
047    private Path m_repository;
048
049    /**
050     * Creates a new RFS image cache.<p>
051     *
052     * @param repository the image cache repository path
053     * @throws Exception if the repository can not be initialized
054     */
055    public CmsRfsImageCache(String repository)
056    throws Exception {
057
058        initRepository(Paths.get(repository).toAbsolutePath().normalize());
059    }
060
061    /**
062     * @see org.opencms.loader.I_CmsImageCache#clear()
063     */
064    @Override
065    public void clear() throws Exception {
066
067        Path repository = getRepository();
068        List<Path> paths;
069        try (Stream<Path> stream = Files.walk(repository)) {
070            paths = stream.filter(path -> !repository.equals(path)).sorted(Comparator.reverseOrder()).collect(
071                Collectors.toList());
072        }
073        for (Path path : paths) {
074            Files.deleteIfExists(path);
075        }
076    }
077
078    /**
079     * @see org.opencms.loader.I_CmsImageCache#exists(java.lang.String)
080     */
081    public boolean exists(String key) throws Exception {
082
083        return Files.isRegularFile(getPath(key), LinkOption.NOFOLLOW_LINKS);
084    }
085
086    /**
087     * @see org.opencms.loader.I_CmsImageCache#getLength(java.lang.String)
088     */
089    public long getLength(String key) throws Exception {
090
091        return Files.size(getPath(key));
092    }
093
094    /**
095     * Returns the absolute repository path.<p>
096     *
097     * @return the repository path
098     */
099    public String getRepositoryPath() {
100
101        if (m_repository == null) {
102            throw new IllegalStateException("Image cache has not been initialized.");
103        }
104        return m_repository.toString();
105    }
106
107    /**
108     * @see org.opencms.loader.I_CmsImageCache#supportsRangeDelivery()
109     */
110    public boolean supportsRangeDelivery() {
111
112        return true;
113    }
114
115    /**
116     * @see org.opencms.loader.I_CmsImageCache#visitEntries(org.opencms.loader.I_CmsImageCache.I_CmsImageCacheEntryVisitor)
117     */
118    @Override
119    public void visitEntries(I_CmsImageCacheEntryVisitor visitor) throws Exception {
120
121        Path repository = getRepository();
122        try (Stream<Path> paths = Files.walk(repository)) {
123            java.util.Iterator<Path> iterator = paths.filter(
124                candidate -> Files.isRegularFile(candidate, LinkOption.NOFOLLOW_LINKS)).iterator();
125            while (iterator.hasNext()) {
126                Path path = iterator.next();
127                String key = repository.relativize(path).toString().replace(path.getFileSystem().getSeparator(), "/");
128                visitor.visit(key, Files.size(path));
129            }
130        }
131    }
132
133    /**
134     * @see org.opencms.loader.I_CmsImageCache#write(java.lang.String, byte[])
135     */
136    public void write(String key, byte[] content) throws Exception {
137
138        Path path = getPath(key);
139        Files.createDirectories(path.getParent());
140        Files.write(path, content);
141    }
142
143    /**
144     * @see org.opencms.loader.I_CmsImageCache#writeRangeTo(java.lang.String, long, long, java.io.OutputStream)
145     */
146    public void writeRangeTo(String key, long start, long length, OutputStream out) throws Exception {
147
148        try (InputStream in = Files.newInputStream(getPath(key))) {
149            skipFully(in, start);
150            byte[] buffer = new byte[8192];
151            long remaining = length;
152            while (remaining > 0) {
153                int read = in.read(buffer, 0, (int)Math.min(buffer.length, remaining));
154                if (read < 0) {
155                    return;
156                }
157                out.write(buffer, 0, read);
158                remaining -= read;
159            }
160        }
161    }
162
163    /**
164     * @see org.opencms.loader.I_CmsImageCache#writeTo(java.lang.String, java.io.OutputStream)
165     */
166    public void writeTo(String key, OutputStream out) throws Exception {
167
168        Files.copy(getPath(key), out);
169    }
170
171    /**
172     * Returns the image cache repository.<p>
173     *
174     * @return the repository
175     */
176    protected Path getRepository() {
177
178        if (m_repository == null) {
179            throw new IllegalStateException("Image cache has not been initialized.");
180        }
181        return m_repository;
182    }
183
184    /**
185     * Initializes the image cache repository.<p>
186     *
187     * @param repository the image cache repository
188     *
189     * @throws Exception if the repository can not be initialized
190     */
191    protected void initRepository(Path repository) throws Exception {
192
193        m_repository = repository;
194        Files.createDirectories(m_repository);
195        if (!Files.isDirectory(m_repository, LinkOption.NOFOLLOW_LINKS)) {
196            throw new IllegalArgumentException("Image cache path is not a directory: " + m_repository);
197        }
198    }
199
200    /**
201     * Returns the cache path for a key.<p>
202     *
203     * @param key the image cache key
204     * @return the cache path
205     */
206    private Path getPath(String key) {
207
208        if (m_repository == null) {
209            throw new IllegalStateException("Image cache has not been initialized.");
210        }
211        String normalizedKey = key;
212        while (normalizedKey.startsWith("/")) {
213            normalizedKey = normalizedKey.substring(1);
214        }
215        Path result = m_repository.resolve(normalizedKey).normalize();
216        if (!result.startsWith(m_repository)) {
217            throw new IllegalArgumentException("Image cache key outside repository: " + key);
218        }
219        return result;
220    }
221
222    /**
223     * Skips exactly the requested number of bytes.<p>
224     *
225     * @param in the input stream
226     * @param bytes the number of bytes to skip
227     * @throws Exception if skipping fails
228     */
229    private void skipFully(InputStream in, long bytes) throws Exception {
230
231        long remaining = bytes;
232        while (remaining > 0) {
233            long skipped = in.skip(remaining);
234            if (skipped > 0) {
235                remaining -= skipped;
236            } else if (in.read() < 0) {
237                return;
238            } else {
239                remaining--;
240            }
241        }
242    }
243}