/*
* 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 org.apache.zeppelin.notebook.repo;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.vfs2.FileContent;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemManager;
import org.apache.commons.vfs2.FileType;
import org.apache.commons.vfs2.NameScope;
import org.apache.commons.vfs2.Selectors;
import org.apache.commons.vfs2.VFS;
import org.apache.zeppelin.conf.ZeppelinConfiguration;
import org.apache.zeppelin.conf.ZeppelinConfiguration.ConfVars;
import org.apache.zeppelin.notebook.ApplicationState;
import org.apache.zeppelin.notebook.Note;
import org.apache.zeppelin.notebook.NoteInfo;
import org.apache.zeppelin.notebook.NotebookImportDeserializer;
import org.apache.zeppelin.notebook.Paragraph;
import org.apache.zeppelin.scheduler.Job.Status;
import org.apache.zeppelin.user.AuthenticationInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Lists;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
*
*/
public class VFSNotebookRepo implements NotebookRepo {
private static final Logger LOG = LoggerFactory.getLogger(VFSNotebookRepo.class);
private FileSystemManager fsManager;
private URI filesystemRoot;
private ZeppelinConfiguration conf;
public VFSNotebookRepo(ZeppelinConfiguration conf) throws IOException {
this.conf = conf;
setNotebookDirectory(conf.getNotebookDir());
}
private void setNotebookDirectory(String notebookDirPath) throws IOException {
try {
if (conf.isWindowsPath(notebookDirPath)) {
filesystemRoot = new File(notebookDirPath).toURI();
} else {
filesystemRoot = new URI(notebookDirPath);
}
} catch (URISyntaxException e1) {
throw new IOException(e1);
}
if (filesystemRoot.getScheme() == null) { // it is local path
File f = new File(conf.getRelativeDir(filesystemRoot.getPath()));
this.filesystemRoot = f.toURI();
}
fsManager = VFS.getManager();
FileObject file = fsManager.resolveFile(filesystemRoot.getPath());
if (!file.exists()) {
LOG.info("Notebook dir doesn't exist, create on is {}.", file.getName());
file.createFolder();
}
}
private String getNotebookDirPath() {
return filesystemRoot.getPath().toString();
}
private String getPath(String path) {
if (path == null || path.trim().length() == 0) {
return filesystemRoot.toString();
}
if (path.startsWith("/")) {
return filesystemRoot.toString() + path;
} else {
return filesystemRoot.toString() + "/" + path;
}
}
private boolean isDirectory(FileObject fo) throws IOException {
if (fo == null) return false;
if (fo.getType() == FileType.FOLDER) {
return true;
} else {
return false;
}
}
@Override
public List<NoteInfo> list(AuthenticationInfo subject) throws IOException {
FileObject rootDir = getRootDir();
FileObject[] children = rootDir.getChildren();
List<NoteInfo> infos = new LinkedList<>();
for (FileObject f : children) {
String fileName = f.getName().getBaseName();
if (f.isHidden()
|| fileName.startsWith(".")
|| fileName.startsWith("#")
|| fileName.startsWith("~")) {
// skip hidden, temporary files
continue;
}
if (!isDirectory(f)) {
// currently single note is saved like, [NOTE_ID]/note.json.
// so it must be a directory
continue;
}
NoteInfo info = null;
try {
info = getNoteInfo(f);
if (info != null) {
infos.add(info);
}
} catch (Exception e) {
LOG.error("Can't read note " + f.getName().toString(), e);
}
}
return infos;
}
private Note getNote(FileObject noteDir) throws IOException {
if (!isDirectory(noteDir)) {
throw new IOException(noteDir.getName().toString() + " is not a directory");
}
FileObject noteJson = noteDir.resolveFile("note.json", NameScope.CHILD);
if (!noteJson.exists()) {
throw new IOException(noteJson.getName().toString() + " not found");
}
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.setPrettyPrinting();
Gson gson = gsonBuilder.registerTypeAdapter(Date.class, new NotebookImportDeserializer())
.create();
FileContent content = noteJson.getContent();
InputStream ins = content.getInputStream();
String json = IOUtils.toString(ins, conf.getString(ConfVars.ZEPPELIN_ENCODING));
ins.close();
Note note = Note.fromJson(json);
// note.setReplLoader(replLoader);
// note.jobListenerFactory = jobListenerFactory;
for (Paragraph p : note.getParagraphs()) {
if (p.getStatus() == Status.PENDING || p.getStatus() == Status.RUNNING) {
p.setStatus(Status.ABORT);
}
List<ApplicationState> appStates = p.getAllApplicationStates();
if (appStates != null) {
for (ApplicationState app : appStates) {
if (app.getStatus() != ApplicationState.Status.ERROR) {
app.setStatus(ApplicationState.Status.UNLOADED);
}
}
}
}
return note;
}
private NoteInfo getNoteInfo(FileObject noteDir) throws IOException {
Note note = getNote(noteDir);
return new NoteInfo(note);
}
@Override
public Note get(String noteId, AuthenticationInfo subject) throws IOException {
FileObject rootDir = fsManager.resolveFile(getPath("/"));
FileObject noteDir = rootDir.resolveFile(noteId, NameScope.CHILD);
return getNote(noteDir);
}
protected FileObject getRootDir() throws IOException {
FileObject rootDir = fsManager.resolveFile(getPath("/"));
if (!rootDir.exists()) {
throw new IOException("Root path does not exists");
}
if (!isDirectory(rootDir)) {
throw new IOException("Root path is not a directory");
}
return rootDir;
}
@Override
public synchronized void save(Note note, AuthenticationInfo subject) throws IOException {
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.setPrettyPrinting();
Gson gson = gsonBuilder.create();
String json = gson.toJson(note);
FileObject rootDir = getRootDir();
FileObject noteDir = rootDir.resolveFile(note.getId(), NameScope.CHILD);
if (!noteDir.exists()) {
noteDir.createFolder();
}
if (!isDirectory(noteDir)) {
throw new IOException(noteDir.getName().toString() + " is not a directory");
}
FileObject noteJson = noteDir.resolveFile(".note.json", NameScope.CHILD);
// false means not appending. creates file if not exists
OutputStream out = noteJson.getContent().getOutputStream(false);
out.write(json.getBytes(conf.getString(ConfVars.ZEPPELIN_ENCODING)));
out.close();
noteJson.moveTo(noteDir.resolveFile("note.json", NameScope.CHILD));
}
@Override
public void remove(String noteId, AuthenticationInfo subject) throws IOException {
FileObject rootDir = fsManager.resolveFile(getPath("/"));
FileObject noteDir = rootDir.resolveFile(noteId, NameScope.CHILD);
if (!noteDir.exists()) {
// nothing to do
return;
}
if (!isDirectory(noteDir)) {
// it is not look like zeppelin note savings
throw new IOException("Can not remove " + noteDir.getName().toString());
}
noteDir.delete(Selectors.SELECT_SELF_AND_CHILDREN);
}
@Override
public void close() {
//no-op
}
@Override
public Revision checkpoint(String noteId, String checkpointMsg, AuthenticationInfo subject)
throws IOException {
// no-op
LOG.warn("Checkpoint feature isn't supported in {}", this.getClass().toString());
return Revision.EMPTY;
}
@Override
public Note get(String noteId, String revId, AuthenticationInfo subject) throws IOException {
LOG.warn("Get note revision feature isn't supported in {}", this.getClass().toString());
return null;
}
@Override
public List<Revision> revisionHistory(String noteId, AuthenticationInfo subject) {
LOG.warn("Get Note revisions feature isn't supported in {}", this.getClass().toString());
return Collections.emptyList();
}
@Override
public List<NotebookRepoSettingsInfo> getSettings(AuthenticationInfo subject) {
NotebookRepoSettingsInfo repoSetting = NotebookRepoSettingsInfo.newInstance();
List<NotebookRepoSettingsInfo> settings = Lists.newArrayList();
repoSetting.name = "Notebook Path";
repoSetting.type = NotebookRepoSettingsInfo.Type.INPUT;
repoSetting.value = Collections.emptyList();
repoSetting.selected = getNotebookDirPath();
settings.add(repoSetting);
return settings;
}
@Override
public void updateSettings(Map<String, String> settings, AuthenticationInfo subject) {
if (settings == null || settings.isEmpty()) {
LOG.error("Cannot update {} with empty settings", this.getClass().getName());
return;
}
String newNotebookDirectotyPath = StringUtils.EMPTY;
if (settings.containsKey("Notebook Path")) {
newNotebookDirectotyPath = settings.get("Notebook Path");
}
if (StringUtils.isBlank(newNotebookDirectotyPath)) {
LOG.error("Notebook path is invalid");
return;
}
LOG.warn("{} will change notebook dir from {} to {}",
subject.getUser(), getNotebookDirPath(), newNotebookDirectotyPath);
try {
setNotebookDirectory(newNotebookDirectotyPath);
} catch (IOException e) {
LOG.error("Cannot update notebook directory", e);
}
}
@Override
public Note setNoteRevision(String noteId, String revId, AuthenticationInfo subject)
throws IOException {
// Auto-generated method stub
return null;
}
}