mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2024-01-19 02:48:24 +00:00
commit
2ff727a32d
1
go.mod
1
go.mod
|
@ -60,6 +60,7 @@ require (
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563 // indirect
|
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563 // indirect
|
||||||
github.com/seaweedfs/fuse v0.0.0-20190510212405-310228904eff
|
github.com/seaweedfs/fuse v0.0.0-20190510212405-310228904eff
|
||||||
github.com/seaweedfs/goexif v1.0.2
|
github.com/seaweedfs/goexif v1.0.2
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
github.com/spf13/afero v1.2.2 // indirect
|
github.com/spf13/afero v1.2.2 // indirect
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -452,6 +452,8 @@ github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
|
||||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||||
github.com/smartystreets/assertions v1.1.1/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
github.com/smartystreets/assertions v1.1.1/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
|
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
|
||||||
github.com/smartystreets/gunit v1.3.4/go.mod h1:ZjM1ozSIMJlAz/ay4SG8PeKF00ckUp+zMHZXV9/bvak=
|
github.com/smartystreets/gunit v1.3.4/go.mod h1:ZjM1ozSIMJlAz/ay4SG8PeKF00ckUp+zMHZXV9/bvak=
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
<groupId>com.github.chrislusf</groupId>
|
<groupId>com.github.chrislusf</groupId>
|
||||||
<artifactId>seaweedfs-client</artifactId>
|
<artifactId>seaweedfs-client</artifactId>
|
||||||
<version>1.4.8</version>
|
<version>1.4.9</version>
|
||||||
|
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>org.sonatype.oss</groupId>
|
<groupId>org.sonatype.oss</groupId>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
<groupId>com.github.chrislusf</groupId>
|
<groupId>com.github.chrislusf</groupId>
|
||||||
<artifactId>seaweedfs-client</artifactId>
|
<artifactId>seaweedfs-client</artifactId>
|
||||||
<version>1.4.8</version>
|
<version>1.4.9</version>
|
||||||
|
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>org.sonatype.oss</groupId>
|
<groupId>org.sonatype.oss</groupId>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
<groupId>com.github.chrislusf</groupId>
|
<groupId>com.github.chrislusf</groupId>
|
||||||
<artifactId>seaweedfs-client</artifactId>
|
<artifactId>seaweedfs-client</artifactId>
|
||||||
<version>1.4.8</version>
|
<version>1.4.9</version>
|
||||||
|
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>org.sonatype.oss</groupId>
|
<groupId>org.sonatype.oss</groupId>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package seaweedfs.client;
|
package seaweedfs.client;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -272,15 +273,20 @@ public class FilerClient {
|
||||||
|
|
||||||
public boolean createEntry(String parent, FilerProto.Entry entry) {
|
public boolean createEntry(String parent, FilerProto.Entry entry) {
|
||||||
try {
|
try {
|
||||||
|
FilerProto.CreateEntryResponse createEntryResponse =
|
||||||
filerGrpcClient.getBlockingStub().createEntry(FilerProto.CreateEntryRequest.newBuilder()
|
filerGrpcClient.getBlockingStub().createEntry(FilerProto.CreateEntryRequest.newBuilder()
|
||||||
.setDirectory(parent)
|
.setDirectory(parent)
|
||||||
.setEntry(entry)
|
.setEntry(entry)
|
||||||
.build());
|
.build());
|
||||||
|
if (Strings.isNullOrEmpty(createEntryResponse.getError())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
LOG.warn("createEntry {}/{} error: {}", parent, entry.getName(), createEntryResponse.getError());
|
||||||
|
return false;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.warn("createEntry {}/{}: {}", parent, entry.getName(), e);
|
LOG.warn("createEntry {}/{}: {}", parent, entry.getName(), e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean updateEntry(String parent, FilerProto.Entry entry) {
|
public boolean updateEntry(String parent, FilerProto.Entry entry) {
|
||||||
|
@ -290,7 +296,7 @@ public class FilerClient {
|
||||||
.setEntry(entry)
|
.setEntry(entry)
|
||||||
.build());
|
.build());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.warn("createEntry {}/{}: {}", parent, entry.getName(), e);
|
LOG.warn("updateEntry {}/{}: {}", parent, entry.getName(), e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -301,7 +301,7 @@
|
||||||
</snapshotRepository>
|
</snapshotRepository>
|
||||||
</distributionManagement>
|
</distributionManagement>
|
||||||
<properties>
|
<properties>
|
||||||
<seaweedfs.client.version>1.4.8</seaweedfs.client.version>
|
<seaweedfs.client.version>1.4.9</seaweedfs.client.version>
|
||||||
<hadoop.version>2.9.2</hadoop.version>
|
<hadoop.version>2.9.2</hadoop.version>
|
||||||
</properties>
|
</properties>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<seaweedfs.client.version>1.4.8</seaweedfs.client.version>
|
<seaweedfs.client.version>1.4.9</seaweedfs.client.version>
|
||||||
<hadoop.version>2.9.2</hadoop.version>
|
<hadoop.version>2.9.2</hadoop.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
|
|
@ -309,7 +309,7 @@
|
||||||
</snapshotRepository>
|
</snapshotRepository>
|
||||||
</distributionManagement>
|
</distributionManagement>
|
||||||
<properties>
|
<properties>
|
||||||
<seaweedfs.client.version>1.4.8</seaweedfs.client.version>
|
<seaweedfs.client.version>1.4.9</seaweedfs.client.version>
|
||||||
<hadoop.version>3.1.1</hadoop.version>
|
<hadoop.version>3.1.1</hadoop.version>
|
||||||
</properties>
|
</properties>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<seaweedfs.client.version>1.4.8</seaweedfs.client.version>
|
<seaweedfs.client.version>1.4.9</seaweedfs.client.version>
|
||||||
<hadoop.version>3.1.1</hadoop.version>
|
<hadoop.version>3.1.1</hadoop.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MountOptions struct {
|
type MountOptions struct {
|
||||||
|
@ -28,6 +29,7 @@ var (
|
||||||
mountOptions MountOptions
|
mountOptions MountOptions
|
||||||
mountCpuProfile *string
|
mountCpuProfile *string
|
||||||
mountMemProfile *string
|
mountMemProfile *string
|
||||||
|
mountReadRetryTime *time.Duration
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -46,11 +48,13 @@ func init() {
|
||||||
mountOptions.allowOthers = cmdMount.Flag.Bool("allowOthers", true, "allows other users to access the file system")
|
mountOptions.allowOthers = cmdMount.Flag.Bool("allowOthers", true, "allows other users to access the file system")
|
||||||
mountOptions.umaskString = cmdMount.Flag.String("umask", "022", "octal umask, e.g., 022, 0111")
|
mountOptions.umaskString = cmdMount.Flag.String("umask", "022", "octal umask, e.g., 022, 0111")
|
||||||
mountOptions.nonempty = cmdMount.Flag.Bool("nonempty", false, "allows the mounting over a non-empty directory")
|
mountOptions.nonempty = cmdMount.Flag.Bool("nonempty", false, "allows the mounting over a non-empty directory")
|
||||||
mountCpuProfile = cmdMount.Flag.String("cpuprofile", "", "cpu profile output file")
|
|
||||||
mountMemProfile = cmdMount.Flag.String("memprofile", "", "memory profile output file")
|
|
||||||
mountOptions.outsideContainerClusterMode = cmdMount.Flag.Bool("outsideContainerClusterMode", false, "allows other users to access the file system")
|
mountOptions.outsideContainerClusterMode = cmdMount.Flag.Bool("outsideContainerClusterMode", false, "allows other users to access the file system")
|
||||||
mountOptions.uidMap = cmdMount.Flag.String("map.uid", "", "map local uid to uid on filer, comma-separated <local_uid>:<filer_uid>")
|
mountOptions.uidMap = cmdMount.Flag.String("map.uid", "", "map local uid to uid on filer, comma-separated <local_uid>:<filer_uid>")
|
||||||
mountOptions.gidMap = cmdMount.Flag.String("map.gid", "", "map local gid to gid on filer, comma-separated <local_gid>:<filer_gid>")
|
mountOptions.gidMap = cmdMount.Flag.String("map.gid", "", "map local gid to gid on filer, comma-separated <local_gid>:<filer_gid>")
|
||||||
|
|
||||||
|
mountCpuProfile = cmdMount.Flag.String("cpuprofile", "", "cpu profile output file")
|
||||||
|
mountMemProfile = cmdMount.Flag.String("memprofile", "", "memory profile output file")
|
||||||
|
mountReadRetryTime = cmdMount.Flag.Duration("readRetryTime", 6*time.Second, "maximum read retry wait time")
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmdMount = &Command{
|
var cmdMount = &Command{
|
||||||
|
|
|
@ -5,6 +5,7 @@ package command
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/chrislusf/seaweedfs/weed/filer"
|
||||||
"github.com/chrislusf/seaweedfs/weed/filesys/meta_cache"
|
"github.com/chrislusf/seaweedfs/weed/filesys/meta_cache"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
|
@ -29,6 +30,10 @@ import (
|
||||||
func runMount(cmd *Command, args []string) bool {
|
func runMount(cmd *Command, args []string) bool {
|
||||||
|
|
||||||
grace.SetupProfiling(*mountCpuProfile, *mountMemProfile)
|
grace.SetupProfiling(*mountCpuProfile, *mountMemProfile)
|
||||||
|
if *mountReadRetryTime < time.Second {
|
||||||
|
*mountReadRetryTime = time.Second
|
||||||
|
}
|
||||||
|
filer.ReadWaitTime = *mountReadRetryTime
|
||||||
|
|
||||||
umask, umaskErr := strconv.ParseUint(*mountOptions.umaskString, 8, 64)
|
umask, umaskErr := strconv.ParseUint(*mountOptions.umaskString, 8, 64)
|
||||||
if umaskErr != nil {
|
if umaskErr != nil {
|
||||||
|
|
|
@ -98,7 +98,7 @@ func retriedFetchChunkData(urlStrings []string, cipherKey []byte, isGzipped bool
|
||||||
var err error
|
var err error
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
for waitTime := time.Second; waitTime < 10*time.Second; waitTime += waitTime / 2 {
|
for waitTime := time.Second; waitTime < ReadWaitTime; waitTime += waitTime / 2 {
|
||||||
for _, urlString := range urlStrings {
|
for _, urlString := range urlStrings {
|
||||||
err = util.ReadUrlAsStream(urlString, cipherKey, isGzipped, isFullChunk, offset, size, func(data []byte) {
|
err = util.ReadUrlAsStream(urlString, cipherKey, isGzipped, isFullChunk, offset, size, func(data []byte) {
|
||||||
buffer.Write(data)
|
buffer.Write(data)
|
||||||
|
|
|
@ -11,6 +11,11 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ReadWaitTime = 6 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
type ChunkReadAt struct {
|
type ChunkReadAt struct {
|
||||||
|
@ -37,7 +42,8 @@ func LookupFn(filerClient filer_pb.FilerClient) LookupFileIdFunctionType {
|
||||||
vid := VolumeId(fileId)
|
vid := VolumeId(fileId)
|
||||||
locations, found := vidCache[vid]
|
locations, found := vidCache[vid]
|
||||||
|
|
||||||
if !found {
|
waitTime := time.Second
|
||||||
|
for !found && waitTime < ReadWaitTime {
|
||||||
// println("looking up volume", vid)
|
// println("looking up volume", vid)
|
||||||
err = filerClient.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
|
err = filerClient.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
|
||||||
resp, err := client.LookupVolume(context.Background(), &filer_pb.LookupVolumeRequest{
|
resp, err := client.LookupVolume(context.Background(), &filer_pb.LookupVolumeRequest{
|
||||||
|
@ -56,6 +62,16 @@ func LookupFn(filerClient filer_pb.FilerClient) LookupFileIdFunctionType {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
glog.V(1).Infof("wait for volume %s", vid)
|
||||||
|
time.Sleep(waitTime)
|
||||||
|
waitTime += waitTime / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, loc := range locations.Locations {
|
for _, loc := range locations.Locations {
|
||||||
|
|
|
@ -34,7 +34,7 @@ func StreamContent(masterClient *wdclient.MasterClient, w io.Writer, chunks []*f
|
||||||
urlStrings := fileId2Url[chunkView.FileId]
|
urlStrings := fileId2Url[chunkView.FileId]
|
||||||
|
|
||||||
data, err := retriedFetchChunkData(urlStrings, chunkView.CipherKey, chunkView.IsGzipped, chunkView.IsFullChunk(), chunkView.Offset, int(chunkView.Size))
|
data, err := retriedFetchChunkData(urlStrings, chunkView.CipherKey, chunkView.IsGzipped, chunkView.IsFullChunk(), chunkView.Offset, int(chunkView.Size))
|
||||||
if err == nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
w.Write(data)
|
w.Write(data)
|
||||||
|
|
|
@ -79,7 +79,7 @@ func (fh *FileHandle) Read(ctx context.Context, req *fuse.ReadRequest, resp *fus
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Warningf("file handle read %s %d: %v", fh.f.fullpath(), totalRead, err)
|
glog.Warningf("file handle read %s %d: %v", fh.f.fullpath(), totalRead, err)
|
||||||
return nil
|
return fuse.EIO
|
||||||
}
|
}
|
||||||
|
|
||||||
if totalRead > int64(len(buff)) {
|
if totalRead > int64(len(buff)) {
|
||||||
|
|
|
@ -2,6 +2,9 @@ package weed_server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"github.com/skip2/go-qrcode"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -65,7 +68,15 @@ func (fs *FilerServer) listDirectoryHandler(w http.ResponseWriter, r *http.Reque
|
||||||
lastFileName,
|
lastFileName,
|
||||||
shouldDisplayLoadMore,
|
shouldDisplayLoadMore,
|
||||||
})
|
})
|
||||||
} else {
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var qrImageString string
|
||||||
|
img, err := qrcode.Encode(fmt.Sprintf("http://%s:%d%s", fs.option.Host, fs.option.Port, r.URL.Path), qrcode.Medium, 128)
|
||||||
|
if err == nil {
|
||||||
|
qrImageString = base64.StdEncoding.EncodeToString(img)
|
||||||
|
}
|
||||||
|
|
||||||
ui.StatusTpl.Execute(w, struct {
|
ui.StatusTpl.Execute(w, struct {
|
||||||
Path string
|
Path string
|
||||||
Breadcrumbs []ui.Breadcrumb
|
Breadcrumbs []ui.Breadcrumb
|
||||||
|
@ -73,6 +84,7 @@ func (fs *FilerServer) listDirectoryHandler(w http.ResponseWriter, r *http.Reque
|
||||||
Limit int
|
Limit int
|
||||||
LastFileName string
|
LastFileName string
|
||||||
ShouldDisplayLoadMore bool
|
ShouldDisplayLoadMore bool
|
||||||
|
QrImage string
|
||||||
}{
|
}{
|
||||||
path,
|
path,
|
||||||
ui.ToBreadcrumb(path),
|
ui.ToBreadcrumb(path),
|
||||||
|
@ -80,6 +92,6 @@ func (fs *FilerServer) listDirectoryHandler(w http.ResponseWriter, r *http.Reque
|
||||||
limit,
|
limit,
|
||||||
lastFileName,
|
lastFileName,
|
||||||
shouldDisplayLoadMore,
|
shouldDisplayLoadMore,
|
||||||
|
qrImageString,
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,8 +22,10 @@ var StatusTpl = template.Must(template.New("status").Funcs(funcMap).Parse(`<!DOC
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>SeaweedFS Filer</title>
|
<title>SeaweedFS Filer</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="/seaweedfsstatic/bootstrap/3.3.1/css/bootstrap.min.css">
|
<link rel="stylesheet" href="/seaweedfsstatic/bootstrap/3.3.1/css/bootstrap.min.css">
|
||||||
<style>
|
<style>
|
||||||
|
body { padding-bottom: 70px; }
|
||||||
#drop-area {
|
#drop-area {
|
||||||
border: 1px transparent;
|
border: 1px transparent;
|
||||||
}
|
}
|
||||||
|
@ -46,6 +48,11 @@ var StatusTpl = template.Must(template.New("status").Funcs(funcMap).Parse(`<!DOC
|
||||||
#fileElem {
|
#fileElem {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.qrImage {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -116,6 +123,14 @@ var StatusTpl = template.Must(template.New("status").Funcs(funcMap).Parse(`<!DOC
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div class="navbar navbar-fixed-bottom">
|
||||||
|
<img src="data:image/png;base64,{{.QrImage}}" class="qrImage" />
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
Loading…
Reference in a new issue