You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							191 lines
						
					
					
						
							6.2 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							191 lines
						
					
					
						
							6.2 KiB
						
					
					
				| package shell | |
| 
 | |
| import ( | |
| 	"context" | |
| 	"flag" | |
| 	"fmt" | |
| 	"github.com/seaweedfs/seaweedfs/weed/filer" | |
| 	"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" | |
| 	"github.com/seaweedfs/seaweedfs/weed/pb/remote_pb" | |
| 	"github.com/seaweedfs/seaweedfs/weed/remote_storage" | |
| 	"github.com/seaweedfs/seaweedfs/weed/util" | |
| 	"io" | |
| ) | |
| 
 | |
| func init() { | |
| 	Commands = append(Commands, &commandRemoteMetaSync{}) | |
| } | |
| 
 | |
| type commandRemoteMetaSync struct { | |
| } | |
| 
 | |
| func (c *commandRemoteMetaSync) Name() string { | |
| 	return "remote.meta.sync" | |
| } | |
| 
 | |
| func (c *commandRemoteMetaSync) Help() string { | |
| 	return `synchronize the local file meta data with the remote file metadata | |
|  | |
| 	# assume a remote storage is configured to name "cloud1" | |
| 	remote.configure -name=cloud1 -type=s3 -s3.access_key=xxx -s3.secret_key=yyy | |
| 	# mount and pull one bucket | |
| 	remote.mount -dir=/xxx -remote=cloud1/bucket | |
|  | |
| 	After mount, if the remote file can be changed,  | |
| 	run this command to synchronize the metadata of the mounted folder or any sub folder | |
|  | |
| 		remote.meta.sync -dir=/xxx | |
| 		remote.meta.sync -dir=/xxx/some/subdir | |
|  | |
| 	This is designed to run regularly. So you can add it to some cronjob. | |
|  | |
| 	If there are no other operations changing remote files, this operation is not needed. | |
|  | |
| ` | |
| } | |
| 
 | |
| func (c *commandRemoteMetaSync) HasTag(CommandTag) bool { | |
| 	return false | |
| } | |
| 
 | |
| func (c *commandRemoteMetaSync) Do(args []string, commandEnv *CommandEnv, writer io.Writer) (err error) { | |
| 
 | |
| 	remoteMetaSyncCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError) | |
| 
 | |
| 	dir := remoteMetaSyncCommand.String("dir", "", "a directory in filer") | |
| 
 | |
| 	if err = remoteMetaSyncCommand.Parse(args); err != nil { | |
| 		return nil | |
| 	} | |
| 
 | |
| 	mappings, localMountedDir, remoteStorageMountedLocation, remoteStorageConf, detectErr := detectMountInfo(commandEnv, writer, *dir) | |
| 	if detectErr != nil { | |
| 		jsonPrintln(writer, mappings) | |
| 		return detectErr | |
| 	} | |
| 
 | |
| 	// pull metadata from remote | |
| 	if err = pullMetadata(commandEnv, writer, util.FullPath(localMountedDir), remoteStorageMountedLocation, util.FullPath(*dir), remoteStorageConf); err != nil { | |
| 		return fmt.Errorf("cache meta data: %w", err) | |
| 	} | |
| 
 | |
| 	return nil | |
| } | |
| 
 | |
| func detectMountInfo(commandEnv *CommandEnv, writer io.Writer, dir string) (*remote_pb.RemoteStorageMapping, string, *remote_pb.RemoteStorageLocation, *remote_pb.RemoteConf, error) { | |
| 	return filer.DetectMountInfo(commandEnv.option.GrpcDialOption, commandEnv.option.FilerAddress, dir) | |
| } | |
| 
 | |
| /* | |
| This function update entry.RemoteEntry if the remote has any changes. | |
|  | |
| To pull remote updates, or created for the first time, the criteria is: | |
|  | |
| 	entry == nil or (entry.RemoteEntry != nil and (entry.RemoteEntry.RemoteTag != remote.RemoteTag or entry.RemoteEntry.RemoteMTime < remote.RemoteMTime )) | |
|  | |
| After the meta pull, the entry.RemoteEntry will have: | |
|  | |
| 	remoteEntry.LastLocalSyncTsNs == 0 | |
| 	Attributes.FileSize = uint64(remoteEntry.RemoteSize) | |
| 	Attributes.Mtime = remoteEntry.RemoteMtime | |
| 	remoteEntry.RemoteTag   = actual remote tag | |
| 	chunks = nil | |
|  | |
| When reading the file content or pulling the file content in "remote.cache", the criteria is: | |
|  | |
| 	Attributes.FileSize > 0 and len(chunks) == 0 | |
|  | |
| After caching the file content, the entry.RemoteEntry will be | |
|  | |
| 	remoteEntry.LastLocalSyncTsNs == time.Now.UnixNano() | |
| 	Attributes.FileSize = uint64(remoteEntry.RemoteSize) | |
| 	Attributes.Mtime = remoteEntry.RemoteMtime | |
| 	chunks = non-empty | |
|  | |
| When "weed filer.remote.sync" to upload local changes to remote, the criteria is: | |
|  | |
| 	Attributes.Mtime > remoteEntry.RemoteMtime | |
|  | |
| Right after "weed filer.remote.sync", the entry.RemoteEntry will be | |
|  | |
| 	remoteEntry.LastLocalSyncTsNs = time.Now.UnixNano() | |
| 	remoteEntry.RemoteSize  = actual remote size, which should equal to entry.Attributes.FileSize | |
| 	remoteEntry.RemoteMtime = actual remote mtime, which should be a little greater than entry.Attributes.Mtime | |
| 	remoteEntry.RemoteTag   = actual remote tag | |
|  | |
| If entry does not exist, need to pull meta | |
| If entry.RemoteEntry == nil, this is a new local change and should not be overwritten | |
|  | |
| 	If entry.RemoteEntry.RemoteTag != remoteEntry.RemoteTag { | |
| 	  the remote version is updated, need to pull meta | |
| 	} | |
| */ | |
| func pullMetadata(commandEnv *CommandEnv, writer io.Writer, localMountedDir util.FullPath, remoteMountedLocation *remote_pb.RemoteStorageLocation, dirToCache util.FullPath, remoteConf *remote_pb.RemoteConf) error { | |
| 
 | |
| 	// visit remote storage | |
| 	remoteStorage, err := remote_storage.GetRemoteStorage(remoteConf) | |
| 	if err != nil { | |
| 		return err | |
| 	} | |
| 
 | |
| 	remote := filer.MapFullPathToRemoteStorageLocation(localMountedDir, remoteMountedLocation, dirToCache) | |
| 
 | |
| 	err = commandEnv.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error { | |
| 		ctx := context.Background() | |
| 		err = remoteStorage.Traverse(remote, func(remoteDir, name string, isDirectory bool, remoteEntry *filer_pb.RemoteEntry) error { | |
| 			localDir := filer.MapRemoteStorageLocationPathToFullPath(localMountedDir, remoteMountedLocation, remoteDir) | |
| 			fmt.Fprint(writer, localDir.Child(name)) | |
| 
 | |
| 			lookupResponse, lookupErr := filer_pb.LookupEntry(context.Background(), client, &filer_pb.LookupDirectoryEntryRequest{ | |
| 				Directory: string(localDir), | |
| 				Name:      name, | |
| 			}) | |
| 			var existingEntry *filer_pb.Entry | |
| 			if lookupErr != nil { | |
| 				if lookupErr != filer_pb.ErrNotFound { | |
| 					return lookupErr | |
| 				} | |
| 			} else { | |
| 				existingEntry = lookupResponse.Entry | |
| 			} | |
| 
 | |
| 			if existingEntry == nil { | |
| 				_, createErr := client.CreateEntry(ctx, &filer_pb.CreateEntryRequest{ | |
| 					Directory: string(localDir), | |
| 					Entry: &filer_pb.Entry{ | |
| 						Name:        name, | |
| 						IsDirectory: isDirectory, | |
| 						Attributes: &filer_pb.FuseAttributes{ | |
| 							FileSize: uint64(remoteEntry.RemoteSize), | |
| 							Mtime:    remoteEntry.RemoteMtime, | |
| 							FileMode: uint32(0644), | |
| 						}, | |
| 						RemoteEntry: remoteEntry, | |
| 					}, | |
| 				}) | |
| 				fmt.Fprintln(writer, " (create)") | |
| 				return createErr | |
| 			} else { | |
| 				if existingEntry.RemoteEntry == nil { | |
| 					// this is a new local change and should not be overwritten | |
| 					fmt.Fprintln(writer, " (skip)") | |
| 					return nil | |
| 				} | |
| 				if existingEntry.RemoteEntry.RemoteETag != remoteEntry.RemoteETag || existingEntry.RemoteEntry.RemoteMtime < remoteEntry.RemoteMtime { | |
| 					// the remote version is updated, need to pull meta | |
| 					fmt.Fprintln(writer, " (update)") | |
| 					return doSaveRemoteEntry(client, string(localDir), existingEntry, remoteEntry) | |
| 				} | |
| 			} | |
| 			fmt.Fprintln(writer, " (skip)") | |
| 			return nil | |
| 		}) | |
| 		return err | |
| 	}) | |
| 
 | |
| 	if err != nil { | |
| 		return err | |
| 	} | |
| 
 | |
| 	return nil | |
| }
 |