1/*
2Copyright (c) 2017-2018 VMware, Inc. All Rights Reserved.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package object
18
19import (
20	"bufio"
21	"bytes"
22	"context"
23	"fmt"
24	"io"
25	"log"
26	"path"
27	"strings"
28
29	"github.com/vmware/govmomi/vim25/progress"
30	"github.com/vmware/govmomi/vim25/soap"
31)
32
33// DatastoreFileManager combines FileManager and VirtualDiskManager to manage files on a Datastore
34type DatastoreFileManager struct {
35	Datacenter         *Datacenter
36	Datastore          *Datastore
37	FileManager        *FileManager
38	VirtualDiskManager *VirtualDiskManager
39
40	Force            bool
41	DatacenterTarget *Datacenter
42}
43
44// NewFileManager creates a new instance of DatastoreFileManager
45func (d Datastore) NewFileManager(dc *Datacenter, force bool) *DatastoreFileManager {
46	c := d.Client()
47
48	m := &DatastoreFileManager{
49		Datacenter:         dc,
50		Datastore:          &d,
51		FileManager:        NewFileManager(c),
52		VirtualDiskManager: NewVirtualDiskManager(c),
53		Force:              force,
54		DatacenterTarget:   dc,
55	}
56
57	return m
58}
59
60func (m *DatastoreFileManager) WithProgress(ctx context.Context, s progress.Sinker) context.Context {
61	return context.WithValue(ctx, m, s)
62}
63
64func (m *DatastoreFileManager) wait(ctx context.Context, task *Task) error {
65	var logger progress.Sinker
66	if s, ok := ctx.Value(m).(progress.Sinker); ok {
67		logger = s
68	}
69	_, err := task.WaitForResult(ctx, logger)
70	return err
71}
72
73// Delete dispatches to the appropriate Delete method based on file name extension
74func (m *DatastoreFileManager) Delete(ctx context.Context, name string) error {
75	switch path.Ext(name) {
76	case ".vmdk":
77		return m.DeleteVirtualDisk(ctx, name)
78	default:
79		return m.DeleteFile(ctx, name)
80	}
81}
82
83// DeleteFile calls FileManager.DeleteDatastoreFile
84func (m *DatastoreFileManager) DeleteFile(ctx context.Context, name string) error {
85	p := m.Path(name)
86
87	task, err := m.FileManager.DeleteDatastoreFile(ctx, p.String(), m.Datacenter)
88	if err != nil {
89		return err
90	}
91
92	return m.wait(ctx, task)
93}
94
95// DeleteVirtualDisk calls VirtualDiskManager.DeleteVirtualDisk
96// Regardless of the Datastore type, DeleteVirtualDisk will fail if 'ddb.deletable=false',
97// so if Force=true this method attempts to set 'ddb.deletable=true' before starting the delete task.
98func (m *DatastoreFileManager) DeleteVirtualDisk(ctx context.Context, name string) error {
99	p := m.Path(name)
100
101	var merr error
102
103	if m.Force {
104		merr = m.markDiskAsDeletable(ctx, p)
105	}
106
107	task, err := m.VirtualDiskManager.DeleteVirtualDisk(ctx, p.String(), m.Datacenter)
108	if err != nil {
109		log.Printf("markDiskAsDeletable(%s): %s", p, merr)
110		return err
111	}
112
113	return m.wait(ctx, task)
114}
115
116// CopyFile calls FileManager.CopyDatastoreFile
117func (m *DatastoreFileManager) CopyFile(ctx context.Context, src string, dst string) error {
118	srcp := m.Path(src)
119	dstp := m.Path(dst)
120
121	task, err := m.FileManager.CopyDatastoreFile(ctx, srcp.String(), m.Datacenter, dstp.String(), m.DatacenterTarget, m.Force)
122	if err != nil {
123		return err
124	}
125
126	return m.wait(ctx, task)
127}
128
129// Copy dispatches to the appropriate FileManager or VirtualDiskManager Copy method based on file name extension
130func (m *DatastoreFileManager) Copy(ctx context.Context, src string, dst string) error {
131	srcp := m.Path(src)
132	dstp := m.Path(dst)
133
134	f := m.FileManager.CopyDatastoreFile
135
136	if srcp.IsVMDK() {
137		// types.VirtualDiskSpec=nil as it is not implemented by vCenter
138		f = func(ctx context.Context, src string, srcDC *Datacenter, dst string, dstDC *Datacenter, force bool) (*Task, error) {
139			return m.VirtualDiskManager.CopyVirtualDisk(ctx, src, srcDC, dst, dstDC, nil, force)
140		}
141	}
142
143	task, err := f(ctx, srcp.String(), m.Datacenter, dstp.String(), m.DatacenterTarget, m.Force)
144	if err != nil {
145		return err
146	}
147
148	return m.wait(ctx, task)
149}
150
151// MoveFile calls FileManager.MoveDatastoreFile
152func (m *DatastoreFileManager) MoveFile(ctx context.Context, src string, dst string) error {
153	srcp := m.Path(src)
154	dstp := m.Path(dst)
155
156	task, err := m.FileManager.MoveDatastoreFile(ctx, srcp.String(), m.Datacenter, dstp.String(), m.DatacenterTarget, m.Force)
157	if err != nil {
158		return err
159	}
160
161	return m.wait(ctx, task)
162}
163
164// Move dispatches to the appropriate FileManager or VirtualDiskManager Move method based on file name extension
165func (m *DatastoreFileManager) Move(ctx context.Context, src string, dst string) error {
166	srcp := m.Path(src)
167	dstp := m.Path(dst)
168
169	f := m.FileManager.MoveDatastoreFile
170
171	if srcp.IsVMDK() {
172		f = m.VirtualDiskManager.MoveVirtualDisk
173	}
174
175	task, err := f(ctx, srcp.String(), m.Datacenter, dstp.String(), m.DatacenterTarget, m.Force)
176	if err != nil {
177		return err
178	}
179
180	return m.wait(ctx, task)
181}
182
183// Path converts path name to a DatastorePath
184func (m *DatastoreFileManager) Path(name string) *DatastorePath {
185	var p DatastorePath
186
187	if !p.FromString(name) {
188		p.Path = name
189		p.Datastore = m.Datastore.Name()
190	}
191
192	return &p
193}
194
195func (m *DatastoreFileManager) markDiskAsDeletable(ctx context.Context, path *DatastorePath) error {
196	r, _, err := m.Datastore.Download(ctx, path.Path, &soap.DefaultDownload)
197	if err != nil {
198		return err
199	}
200
201	defer r.Close()
202
203	hasFlag := false
204	buf := new(bytes.Buffer)
205
206	s := bufio.NewScanner(&io.LimitedReader{R: r, N: 2048}) // should be only a few hundred bytes, limit to be sure
207
208	for s.Scan() {
209		line := s.Text()
210		if strings.HasPrefix(line, "ddb.deletable") {
211			hasFlag = true
212			continue
213		}
214
215		fmt.Fprintln(buf, line)
216	}
217
218	if err := s.Err(); err != nil {
219		return err // any error other than EOF
220	}
221
222	if !hasFlag {
223		return nil // already deletable, so leave as-is
224	}
225
226	// rewrite the .vmdk with ddb.deletable flag removed (the default is true)
227	return m.Datastore.Upload(ctx, buf, path.Path, &soap.DefaultUpload)
228}
229